Implement Ghana tax brackets and fix cedi currency display#12
Conversation
…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>
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
|
@lord-shola is attempting to deploy a commit to the mhaadi Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis pull request introduces Ghana-specific progressive income tax calculations and updates currency formatting to be locale-aware. A new Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorLegacy tax values need version checking or recalculation.
legacy?.taxLiabilitytakes precedence overcalculateGhanaTax()at lines 334–336. Since the schema'scalculationVersionfield only exists on new tax estimate entries (marked'ghana-progressive-v1'inconvex/influencers.ts), old legacy entries without this field likely contain the flat 25% rate. Channels migrated with stalelegacy.taxLiabilitywill continue displaying the old flat-rate estimate instead of the new progressive calculation.Either:
- Recalculate
legacy.taxLiabilitybefore display ifcalculationVersionis missing or absent, or- Check
calculationVersionon the source entry and skip legacy tax if it predates the progressive method, or- 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 | 🟠 MajorPersist the fallback revenue into the analytics snapshot.
The fallback RPM value is only assigned to
taxableRevenue, whilesyncPayload.estimatedRevenueremainsargs.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/Mvalues currently usetoFixed(1), while small values usecurrencyConfig.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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (10)
convex/channelData.tsconvex/influencers.tsconvex/tax.tspackage.jsonsrc/__tests__/currency.test.tssrc/__tests__/tax.test.tssrc/app/(dashboard)/analytics/page.tsxsrc/app/(dashboard)/channel-lookup/page.tsxsrc/app/(dashboard)/influencers/page.tsxsrc/lib/revenue-estimate.ts
| 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', |
There was a problem hiding this comment.
🧩 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 -A3Repository: 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 -A10Repository: mhaadiabu/graitld
Length of output: 12685
🏁 Script executed:
# Check if there's any annualization happening after tax calculation
rg -n 'estimateAnnualRevenueFromAnalytics' --type=ts -B3 -A3Repository: 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
vitesttesting framework for the project. The most important changes are summarized below:Ghana Tax Calculation Updates:
calculateGhanaTaxfunction inconvex/tax.tsto compute annual income tax using Ghana Revenue Authority's progressive tax brackets, along with aneffectiveTaxRatehelper. All tax calculations throughout the codebase now use this logic instead of a flat rate. [1] [2] [3] [4] [5]Revenue Formatting Improvements:
formatEstimatedRevenueUsdwithformatEstimatedRevenue, 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:
vitesttests for both the Ghana tax calculation logic (src/__tests__/tax.test.ts) and revenue formatting (src/__tests__/currency.test.ts). [1] [2]vitestinto the project for running and watching tests, updatingpackage.jsonscripts and dependencies. [1] [2]Fallback Revenue Estimation:
Imports and Code Cleanup:
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
Improvements
Tests