Skip to content

Implement Ghana tax brackets and fix cedi currency display#12

Merged
mhaadiabu merged 4 commits into
mhaadiabu:mainfrom
lord-shola:main
Apr 19, 2026
Merged

Implement Ghana tax brackets and fix cedi currency display#12
mhaadiabu merged 4 commits into
mhaadiabu:mainfrom
lord-shola:main

Conversation

@lord-shola

@lord-shola lord-shola commented Apr 19, 2026

Copy link
Copy Markdown

This pull request updates the tax calculation logic to use Ghana's progressive income tax brackets, replaces USD revenue formatting with Ghanaian cedi (GH₵), and adds comprehensive tests for both tax and currency formatting. It also introduces the vitest testing framework for the project. The most important changes are summarized below:

Ghana Tax Calculation Updates:

  • Added a new calculateGhanaTax function in convex/tax.ts to compute annual income tax using Ghana Revenue Authority's progressive tax brackets, along with an effectiveTaxRate helper. All tax calculations throughout the codebase now use this logic instead of a flat rate. [1] [2] [3] [4] [5]
  • Updated tax calculation versioning and notes to reflect the use of the Ghana progressive system. [1] [2]

Revenue Formatting Improvements:

  • Replaced all uses of formatEstimatedRevenueUsd with formatEstimatedRevenue, which now formats values with the GH₵ symbol and supports K/M suffixes for thousands/millions. The old USD formatting is deprecated. [1] [2] [3] [4] [5] [6] [7]

Test Coverage and Tooling:

  • Added comprehensive vitest tests for both the Ghana tax calculation logic (src/__tests__/tax.test.ts) and revenue formatting (src/__tests__/currency.test.ts). [1] [2]
  • Integrated vitest into the project for running and watching tests, updating package.json scripts and dependencies. [1] [2]

Fallback Revenue Estimation:

  • Improved analytics sync logic to always estimate tax, even if only view counts are available, by applying a fallback RPM (revenue per mille) value. [1] [2]

Imports and Code Cleanup:

  • Updated imports across modules to use the new tax and formatting utilities, ensuring consistency and code clarity. [1] [2] [3]

Let me know if you want a walkthrough of the new tax calculation logic or have questions about the test coverage!

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented Ghana-specific progressive income tax calculation system with tiered tax brackets for improved accuracy.
  • Improvements

    • Revenue display now uses Ghana Cedi (GH₵) currency symbol for all monetary values.
    • Tax estimates updated to use progressive tax rates based on income brackets rather than flat rates.
  • Tests

    • Added comprehensive test coverage for tax calculations and currency formatting.

Copilot AI and others added 4 commits April 19, 2026 16:57
…ax estimation

Agent-Logs-Url: https://github.com/lord-shola/graitld/sessions/d318e96e-f2c5-457d-ae66-705eb15c2f38

Co-authored-by: lord-shola <154298518+lord-shola@users.noreply.github.com>
…x-estimation

Fix Ghana cedi display, implement progressive GRA tax brackets, always derive tax from both revenue paths
@vercel

vercel Bot commented Apr 19, 2026

Copy link
Copy Markdown

@lord-shola is attempting to deploy a commit to the mhaadi Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces Ghana-specific progressive income tax calculations and updates currency formatting to be locale-aware. A new tax.ts module implements bracket-based tax logic, while tax estimation in core files is refactored to use these functions instead of a fixed rate. Revenue formatting is updated to display locale-appropriate currency symbols.

Changes

Cohort / File(s) Summary
Tax Calculation Logic
convex/tax.ts, convex/channelData.ts, convex/influencers.ts
Introduced new progressive tax bracket logic with calculateGhanaTax() and effectiveTaxRate() functions; replaced legacy constant-rate tax calculations in channel data and influencer modules with Ghana-specific tax helpers, updated calculation version to 'ghana-progressive-v1', and added fallback tax derivation from view-count RPM.
Currency & Revenue Formatting
src/lib/revenue-estimate.ts, src/app/(dashboard)/analytics/page.tsx, src/app/(dashboard)/channel-lookup/page.tsx, src/app/(dashboard)/influencers/page.tsx
Renamed formatEstimatedRevenueUsd to formatEstimatedRevenue with locale-aware currency symbol and numeric formatting; maintained deprecated alias for backward compatibility; updated all UI pages to use the new formatter.
Testing & Configuration
package.json, src/__tests__/tax.test.ts, src/__tests__/currency.test.ts
Added vitest dev dependency with test and test:watch npm scripts; created comprehensive test suites validating Ghana tax bracket calculations across income ranges and currency formatting with GH₵ symbol.

Sequence Diagram

sequenceDiagram
    participant UI as UI Component
    participant Revenue as Revenue Estimator
    participant Tax as Tax Calculator
    participant Format as Currency Formatter
    
    UI->>Revenue: estimateRevenueFromViews(views)
    Revenue-->>UI: estimatedRevenue
    
    Tax->>Tax: calculateGhanaTax(estimatedRevenue)
    Tax-->>Tax: Apply progressive brackets<br/>Sum marginal taxes
    
    UI->>Format: formatEstimatedRevenue(estimatedRevenue)
    Format->>Format: currencyConfig.symbol<br/>currencyConfig.locale
    Format-->>UI: Formatted: GH₵ X.XK/M
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A taxing tale unfolds today,
Progressive brackets light the way,
From GHS symbols bright and bold,
To Ghana's revenue, truth be told,
Calculations flow with rabbit's paw! 💚

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: implementing Ghana tax brackets and fixing cedi currency display, both of which are core to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
convex/channelData.ts (1)

332-337: ⚠️ Potential issue | 🟠 Major

Legacy tax values need version checking or recalculation.

legacy?.taxLiability takes precedence over calculateGhanaTax() at lines 334–336. Since the schema's calculationVersion field only exists on new tax estimate entries (marked 'ghana-progressive-v1' in convex/influencers.ts), old legacy entries without this field likely contain the flat 25% rate. Channels migrated with stale legacy.taxLiability will continue displaying the old flat-rate estimate instead of the new progressive calculation.

Either:

  1. Recalculate legacy.taxLiability before display if calculationVersion is missing or absent, or
  2. Check calculationVersion on the source entry and skip legacy tax if it predates the progressive method, or
  3. Reorder the fallback to prioritize calculateGhanaTax() over unversioned legacy values.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/channelData.ts` around lines 332 - 337, The fallback logic assigning
estimatedTax should avoid trusting unversioned legacy.taxLiability; update the
calculation in the block that sets estimatedTax (which currently reads
latestTaxEstimate?.estimatedTax ?? legacy?.taxLiability ??
(estimatedAnnualRevenue !== undefined ?
calculateGhanaTax(estimatedAnnualRevenue) : undefined)) to check the source
entry's calculationVersion (or latestTaxEstimate.calculationVersion) and only
use legacy.taxLiability when calculationVersion === 'ghana-progressive-v1' (or
another explicit allowed version); otherwise either call
calculateGhanaTax(estimatedAnnualRevenue) when revenue is available or skip
legacy.taxLiability, ensuring calculateGhanaTax is preferred for unversioned/old
entries.
convex/influencers.ts (1)

239-271: ⚠️ Potential issue | 🟠 Major

Persist the fallback revenue into the analytics snapshot.

The fallback RPM value is only assigned to taxableRevenue, while syncPayload.estimatedRevenue remains args.estimatedRevenue. For views-only syncs, this creates a tax estimate from fallback revenue but still stores no estimated revenue on the analytics sync, so revenue displays/rollups can remain empty despite the PR objective to always estimate revenue.

🐛 Proposed fix
+  const FALLBACK_RPM_GHS = 4; // GHS per 1,000 views (conservative built-in estimate)
+  const fallbackEstimatedRevenue =
+    args.views !== undefined ? (args.views / 1000) * FALLBACK_RPM_GHS : undefined;
+  const taxableRevenue = args.estimatedRevenue ?? fallbackEstimatedRevenue;
+
   const matchingSync = existingSyncs.find((entry) => entry.periodEnd === args.periodEnd);
   const syncPayload = {
     channelId: args.channelId,
     connectionId: args.connectionId,
     periodStart: args.periodStart,
     periodEnd: args.periodEnd,
-    estimatedRevenue: args.estimatedRevenue,
+    estimatedRevenue: taxableRevenue,
     estimatedAdRevenue: args.estimatedAdRevenue,
     estimatedRedRevenue: args.estimatedRedRevenue,
     monetizedPlaybacks: args.monetizedPlaybacks,
     cpm: args.cpm,
@@
-  // Use actual analytics revenue when available; fall back to a view-count
-  // RPM estimate so tax is always derived regardless of revenue source.
-  const FALLBACK_RPM_GHS = 4; // GHS per 1,000 views (conservative built-in estimate)
-  const taxableRevenue =
-    args.estimatedRevenue ??
-    (args.views !== undefined ? (args.views / 1000) * FALLBACK_RPM_GHS : undefined);
-
   if (taxableRevenue !== undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/influencers.ts` around lines 239 - 271, The sync currently computes a
fallback taxableRevenue but does not persist that fallback into syncPayload.
Change the flow so you compute a single estimatedRevenueValue (use
args.estimatedRevenue ?? (args.views !== undefined ? (args.views/1000) *
FALLBACK_RPM_GHS : undefined)) and assign that value to both taxableRevenue and
syncPayload.estimatedRevenue before inserting/updating via ctx.db (symbols:
syncPayload, taxableRevenue, FALLBACK_RPM_GHS, matchingSync,
ctx.db.insert/patch). This ensures view-only syncs store the inferred
estimatedRevenue as well as use it for tax calculations.
🧹 Nitpick comments (2)
src/lib/revenue-estimate.ts (1)

43-50: Apply locale formatting to the compact branches too.

K/M values currently use toFixed(1), while small values use currencyConfig.locale. Keeping all numeric branches locale-aware avoids drift if the configured locale changes.

♻️ Proposed compact-format refactor
 export function formatEstimatedRevenue(value: number): string {
+  const compact = (divisor: number, suffix: string) =>
+    `${currencyConfig.symbol} ${(value / divisor).toLocaleString(currencyConfig.locale, {
+      minimumFractionDigits: 1,
+      maximumFractionDigits: 1,
+    })}${suffix}`;
+
   if (value >= 1_000_000) {
-    return `${currencyConfig.symbol} ${(value / 1_000_000).toFixed(1)}M`;
+    return compact(1_000_000, 'M');
   }
   if (value >= 1_000) {
-    return `${currencyConfig.symbol} ${(value / 1_000).toFixed(1)}K`;
+    return compact(1_000, 'K');
   }
   return `${currencyConfig.symbol} ${Math.round(value).toLocaleString(currencyConfig.locale)}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/revenue-estimate.ts` around lines 43 - 50, The compact branches in
formatEstimatedRevenue are using toFixed(1) which ignores currencyConfig.locale;
change those branches to format the scaled numbers with locale-aware formatting
(e.g., use (value/1_000_000).toLocaleString(currencyConfig.locale, {
maximumFractionDigits: 1 }) for the "M" branch and
(value/1_000).toLocaleString(currencyConfig.locale, { maximumFractionDigits: 1
}) for the "K" branch) while keeping the currencyConfig.symbol and unit
suffixes; leave the small-value branch as-is but ensure all numeric formatting
consistently uses currencyConfig.locale.
convex/tax.ts (1)

1-10: Add effective-year/source metadata for the hardcoded brackets.

These bracket values are correct for 2026 resident individuals and match official GRA sources (confirmed as current as of April 2026). Adding tax-year and source documentation will help with maintenance during future rate changes.

♻️ Proposed metadata addition
-// Ghana Revenue Authority progressive income tax brackets (annual, GHS)
+// Ghana Revenue Authority progressive income tax brackets for resident individuals (annual, GHS).
+// Effective tax schedule: verify against the official GRA schedule before each tax-year rollover.
+export const GHANA_TAX_CALCULATION_VERSION = 'ghana-progressive-v1';
+export const GHANA_TAX_BRACKETS_EFFECTIVE_YEAR = 2026;
 const GHA_TAX_BRACKETS: Array<{ limit: number; rate: number }> = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/tax.ts` around lines 1 - 10, Add tax-year and source metadata for the
hardcoded Ghana brackets by updating the GHA_TAX_BRACKETS definition to include
a comment or adjacent constant that documents the effective tax year and
authoritative source URL; specifically, annotate GHA_TAX_BRACKETS (and/or create
a companion constant like GHA_TAX_METADATA) with "effectiveYear: 2026" and a
source string pointing to the Ghana Revenue Authority page used (include
retrieval date, e.g., "source: 'GRA — Income Tax Rates (accessed Apr 2026)' and
URL"), so future maintainers can see the year and source alongside the bracket
data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/influencers.ts`:
- Around line 285-301: The code is passing the snapshot taxableRevenue (30-day
analytics) into calculateGhanaTax and effectiveTaxRate which expect annual
income; change to annualize the analytics revenue first (use
estimateAnnualRevenueFromAnalytics from channelData.ts with the same
args/periodStart/periodEnd or compute annualizedRevenue =
estimateAnnualRevenueFromAnalytics(taxableRevenue, args.periodStart,
args.periodEnd) / use its return) and then pass that annualized value into
calculateGhanaTax(annualizedRevenue) and effectiveTaxRate(annualizedRevenue),
while keeping taxPayload.grossRevenue/taxableIncome set to the annualized amount
(or include both monthly and annual fields if needed) so Ghana PIT is computed
on yearly income.

---

Outside diff comments:
In `@convex/channelData.ts`:
- Around line 332-337: The fallback logic assigning estimatedTax should avoid
trusting unversioned legacy.taxLiability; update the calculation in the block
that sets estimatedTax (which currently reads latestTaxEstimate?.estimatedTax ??
legacy?.taxLiability ?? (estimatedAnnualRevenue !== undefined ?
calculateGhanaTax(estimatedAnnualRevenue) : undefined)) to check the source
entry's calculationVersion (or latestTaxEstimate.calculationVersion) and only
use legacy.taxLiability when calculationVersion === 'ghana-progressive-v1' (or
another explicit allowed version); otherwise either call
calculateGhanaTax(estimatedAnnualRevenue) when revenue is available or skip
legacy.taxLiability, ensuring calculateGhanaTax is preferred for unversioned/old
entries.

In `@convex/influencers.ts`:
- Around line 239-271: The sync currently computes a fallback taxableRevenue but
does not persist that fallback into syncPayload. Change the flow so you compute
a single estimatedRevenueValue (use args.estimatedRevenue ?? (args.views !==
undefined ? (args.views/1000) * FALLBACK_RPM_GHS : undefined)) and assign that
value to both taxableRevenue and syncPayload.estimatedRevenue before
inserting/updating via ctx.db (symbols: syncPayload, taxableRevenue,
FALLBACK_RPM_GHS, matchingSync, ctx.db.insert/patch). This ensures view-only
syncs store the inferred estimatedRevenue as well as use it for tax
calculations.

---

Nitpick comments:
In `@convex/tax.ts`:
- Around line 1-10: Add tax-year and source metadata for the hardcoded Ghana
brackets by updating the GHA_TAX_BRACKETS definition to include a comment or
adjacent constant that documents the effective tax year and authoritative source
URL; specifically, annotate GHA_TAX_BRACKETS (and/or create a companion constant
like GHA_TAX_METADATA) with "effectiveYear: 2026" and a source string pointing
to the Ghana Revenue Authority page used (include retrieval date, e.g., "source:
'GRA — Income Tax Rates (accessed Apr 2026)' and URL"), so future maintainers
can see the year and source alongside the bracket data.

In `@src/lib/revenue-estimate.ts`:
- Around line 43-50: The compact branches in formatEstimatedRevenue are using
toFixed(1) which ignores currencyConfig.locale; change those branches to format
the scaled numbers with locale-aware formatting (e.g., use
(value/1_000_000).toLocaleString(currencyConfig.locale, { maximumFractionDigits:
1 }) for the "M" branch and (value/1_000).toLocaleString(currencyConfig.locale,
{ maximumFractionDigits: 1 }) for the "K" branch) while keeping the
currencyConfig.symbol and unit suffixes; leave the small-value branch as-is but
ensure all numeric formatting consistently uses currencyConfig.locale.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bfed251e-80c4-4f3a-aab1-e6931cffdba8

📥 Commits

Reviewing files that changed from the base of the PR and between a791127 and 9f2aa3c.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (10)
  • convex/channelData.ts
  • convex/influencers.ts
  • convex/tax.ts
  • package.json
  • src/__tests__/currency.test.ts
  • src/__tests__/tax.test.ts
  • src/app/(dashboard)/analytics/page.tsx
  • src/app/(dashboard)/channel-lookup/page.tsx
  • src/app/(dashboard)/influencers/page.tsx
  • src/lib/revenue-estimate.ts

Comment thread convex/influencers.ts
Comment on lines +285 to +301
const estimatedTax = calculateGhanaTax(taxableRevenue);
const taxPayload = {
channelId: args.channelId,
periodStart: args.periodStart,
periodEnd: args.periodEnd,
sourceType: 'analytics' as const,
manualFinancialId: undefined,
analyticsSyncId: syncId,
grossRevenue: args.estimatedRevenue,
grossRevenue: taxableRevenue,
allowableDeductions: undefined,
taxableIncome: args.estimatedRevenue,
taxRate: DEFAULT_TAX_RATE,
taxableIncome: taxableRevenue,
taxRate: effectiveTaxRate(taxableRevenue),
currency: 'GHS',
estimatedTax: Math.round(args.estimatedRevenue * DEFAULT_TAX_RATE),
estimatedTax,
calculatedAt: Date.now(),
calculatedBy: args.calculatedBy,
calculationVersion: 'wave-1',
notes: 'Derived from connected YouTube analytics',
calculationVersion: 'ghana-progressive-v1',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect analytics snapshot call sites and period semantics.
# Expected: Callers either always pass annual periods/revenue, or the code path needs annualization before calculateGhanaTax/effectiveTaxRate.

rg -nP --type=ts -C5 '\bcompleteChannelAnalyticsConnection\b|\bupsertAnalyticsSyncSnapshot\b|\bperiodStart\b|\bperiodEnd\b|\bestimatedRevenue\b'

Repository: mhaadiabu/graitld

Length of output: 20695


🏁 Script executed:

# Find all callers of upsertAnalyticsSyncSnapshot
rg -n 'upsertAnalyticsSyncSnapshot' --type=ts -B3 -A3

Repository: mhaadiabu/graitld

Length of output: 779


🏁 Script executed:

# Check the calculateGhanaTax function to confirm it's annual-based
rg -n 'calculateGhanaTax|effectiveTaxRate' --type=ts -B5 -A10

Repository: mhaadiabu/graitld

Length of output: 12685


🏁 Script executed:

# Check if there's any annualization happening after tax calculation
rg -n 'estimateAnnualRevenueFromAnalytics' --type=ts -B3 -A3

Repository: mhaadiabu/graitld

Length of output: 1137


Annualize analytics revenue before applying Ghana PIT brackets.

calculateGhanaTax and effectiveTaxRate are documented to take annual income (tax.ts), but this code path applies them directly to the analytics snapshot revenue without annualization. YouTube analytics snapshots are 30-day windows (callback/route.ts lines 90–91). Applying annual progressive brackets to monthly revenue materially understates the tax estimate by ~12×.

Ghana PIT is assessed on yearly chargeable income per GRA guidance. Annualization logic already exists in estimateAnnualRevenueFromAnalytics() (channelData.ts:121–137) and correctly detects period duration; integrate it here or add inline annualization before tax calculation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/influencers.ts` around lines 285 - 301, The code is passing the
snapshot taxableRevenue (30-day analytics) into calculateGhanaTax and
effectiveTaxRate which expect annual income; change to annualize the analytics
revenue first (use estimateAnnualRevenueFromAnalytics from channelData.ts with
the same args/periodStart/periodEnd or compute annualizedRevenue =
estimateAnnualRevenueFromAnalytics(taxableRevenue, args.periodStart,
args.periodEnd) / use its return) and then pass that annualized value into
calculateGhanaTax(annualizedRevenue) and effectiveTaxRate(annualizedRevenue),
while keeping taxPayload.grossRevenue/taxableIncome set to the annualized amount
(or include both monthly and annual fields if needed) so Ghana PIT is computed
on yearly income.

@vercel

vercel Bot commented Apr 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
graitld Ready Ready Preview, Comment Apr 19, 2026 5:32pm

@mhaadiabu mhaadiabu merged commit a54d2a6 into mhaadiabu:main Apr 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants