11 KiB
Tax Metrics — Withholding-Tax Field Schema
Owner: Tharun Swaminathan Iteration: 1 — Design & Data Modelling Date: September 16, 2026
Objective
The purpose of this design is to define how dividend withholding tax should be represented in Ghostfolio before implementation begins in Iteration 2.
The design builds on the existing Ghostfolio activity model instead of creating a separate dividend transaction structure.
This task defines the proposed fields, calculation relationships, validation rules, currency behavior, backward compatibility, and future API impact.
No database or application code is changed as part of this task.
Existing Dividend Model
Ghostfolio currently stores dividend transactions as normal Order activities
with:
type = DIVIDEND
The existing activity already contains the main values needed for a dividend, including:
quantityunitPricecurrencydatefee
The dividend value can be determined from:
grossDividend = quantity × unitPrice
The existing fee field is a general activity fee and should remain separate
from dividend withholding tax.
Proposed Stored Field
The proposed new field on the existing Prisma Order model is:
withholdingTax Float?
Meaning
withholdingTax represents the actual tax amount withheld from a dividend
payment.
The amount is stored in the activity's currency.
Example:
If the dividend activity uses USD, a value of:
withholdingTax = 15.00
means that 15.00 USD was withheld from that dividend payment.
Why the Amount Is Stored
The withholding-tax amount should be treated as the authoritative value because it represents the actual amount deducted from the dividend.
This also provides better traceability for imported transactions and future tax reports.
The withholding percentage can be calculated from the stored amount when needed, so storing both the amount and percentage would introduce the possibility of inconsistent values.
Null and Zero Behavior
The field should be nullable.
withholdingTax = null
means that withholding information is unknown or was not provided.
withholdingTax = 0
means that withholding information is known and no tax was withheld.
This distinction is important for imported and historical transactions.
Missing withholding information should not automatically be interpreted as zero.
This allows existing Ghostfolio dividend records to remain valid after the new field is introduced.
Derived Values
The tax feature should keep gross dividend, withholding tax, withholding rate, and net dividend as separate values.
Gross Dividend
grossDividend = quantity × unitPrice
The gross dividend is derived from the existing activity fields.
Withholding-Tax Amount
withholdingTax
This is the stored value.
Withholding-Tax Rate
The withholding rate is derived rather than stored:
withholdingTaxRate = withholdingTax / grossDividend × 100
The rate can only be calculated when:
grossDividend > 0withholdingTaxis not null
Otherwise:
withholdingTaxRate = null
Net Dividend
The net dividend after withholding is:
netDividend = grossDividend - withholdingTax
If withholdingTax is unknown (null), the tax analytics layer should not
silently assume that withholding was zero.
In that case:
netDividend = null
for tax reporting purposes.
This preserves the difference between "no tax was withheld" and "we do not know whether tax was withheld."
Activity Fee
Activity fees and withholding tax should remain separate.
For a dividend:
grossDividend = quantity × unitPrice
netDividend = grossDividend - withholdingTax
If the dividend also has an activity fee, the cash effect can later be expressed separately as:
netCashAmount = netDividend - fee
This prevents brokerage fees from being treated as tax.
Proposed Prisma Change
The future Iteration 2 implementation is expected to extend the existing
Order model with:
withholdingTax Float?
This is a proposed design only.
Using Float? follows the existing Ghostfolio storage pattern used for numeric
activity fields such as fee, quantity, and unitPrice.
Changing the wider application to a different database numeric type would be a shared architecture change and is outside the scope of this tax task.
Validation Rules
The following validation rules should apply when the feature is implemented:
withholdingTaxis optional.- If provided,
withholdingTaxmust be numeric. - The value must be greater than or equal to zero.
- Withholding tax is only valid for activities where
type = DIVIDEND. - For a valid dividend,
withholdingTax <= grossDividend. - A negative withholding amount is invalid.
- A positive withholding amount on a zero-value dividend is invalid.
- If an activity containing withholding tax is changed from
DIVIDENDto another activity type, the update should be rejected until the withholding value is removed.
Rejecting an invalid combination is preferred over silently deleting tax data.
Currency Behavior
The stored withholdingTax value uses the same transaction currency as the
activity's existing currency field.
For example, currency = USD with withholdingTax = 15 means that 15 USD was
withheld.
The future tax implementation should follow Ghostfolio's existing currency-conversion approach for reporting values.
The enriched activity response may later expose:
withholdingTaxInAssetProfileCurrencywithholdingTaxInBaseCurrency
These should be derived values rather than additional persisted tax amounts.
The original stored withholding amount should remain unchanged so the value continues to match the source transaction.
Proposed DTO Changes
During Iteration 2, the existing activity DTOs would need to accept the new field.
CreateOrderDto
@IsNumber()
@Min(0)
@IsOptional()
withholdingTax?: number | null;
UpdateOrderDto
@IsNumber()
@Min(0)
@IsOptional()
withholdingTax?: number | null;
Cross-field validation that depends on the activity type and gross dividend should remain in the service/business-logic layer because it uses multiple activity fields.
Proposed API Response Values
The stored activity would contain:
withholdingTax
The enriched activity response may later include:
withholdingTaxRatewithholdingTaxInAssetProfileCurrencywithholdingTaxInBaseCurrencynetDividend
These should be calculated values instead of additional persisted database fields.
The existing meaning of Ghostfolio's activity value should not be changed as
part of this feature unless a later integration decision explicitly requires
it.
This reduces the risk of changing existing portfolio calculations while adding tax analytics.
Example 1 — Dividend With Withholding
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
fee = 0
withholdingTax = 15.00
grossDividend = 10 × 10.00
= 100.00 USD
withholdingTax = 15.00 USD
withholdingTaxRate = 15 / 100 × 100
= 15%
netDividend = 100.00 - 15.00
= 85.00 USD
Example 2 — Explicitly No Withholding
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
withholdingTax = 0
grossDividend = 100.00 USD
withholdingTax = 0.00 USD
withholdingTaxRate = 0%
netDividend = 100.00 USD
This means the system knows that no withholding tax was applied.
Example 3 — Historical Dividend With Unknown Withholding
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
withholdingTax = null
grossDividend = 100.00 USD
withholdingTax = unknown
withholdingTaxRate = null
netDividend = null
The existing dividend remains valid, but the tax analytics layer does not pretend that the unknown withholding value is zero.
Import and Backward Compatibility
Existing Ghostfolio dividend activities do not currently contain withholding-tax information.
The new field therefore needs to be nullable so existing database records, imports, and historical transactions remain valid.
Older imports that do not contain the field should result in:
withholdingTax = null
New imports may provide the field when the source data contains an actual withholding amount.
The absence of the field must not cause an otherwise valid dividend import to fail.
Calculation Precision
The stored field should follow the existing Ghostfolio numeric model for compatibility.
Financial calculations involving withholding should use the same high-precision calculation approach already used by Ghostfolio where practical rather than relying on repeated raw floating-point arithmetic.
Rounding should not be applied unnecessarily during intermediate calculations.
Display and export rounding rules will be defined separately with the tax export design.
Edge Cases
The implementation and future tests should cover:
- withholding amount is null
- withholding amount is zero
- normal positive withholding amount
- withholding equal to the full gross dividend
- withholding greater than the gross dividend
- negative withholding amount
- zero-value dividend with positive withholding
- withholding supplied for a non-dividend activity
- imported historical dividend without withholding information
- dividend containing both an activity fee and withholding tax
- foreign-currency dividend
- activity updated from
DIVIDENDto another activity type
Acceptance Criteria
This design is complete when:
- the withholding-tax stored field is clearly defined
- null and zero have different documented meanings
- the authoritative stored value is identified
- the withholding rate is defined as a derived value
- gross and net dividend formulas are documented
- withholding tax remains separate from activity fees
- validation rules are defined
- currency behavior is defined
- existing and imported dividends remain backward compatible
- expected DTO and API impact is documented
- important edge cases are identified
- no jurisdiction-specific tax rules are introduced
Implementation Impact — Iteration 2
The Iteration 2 implementation is expected to affect:
prisma/schema.prismalibs/common/src/lib/dtos/create-order.dto.tslibs/common/src/lib/dtos/update-order.dto.tsapps/api/src/app/activities/activities.service.tslibs/common/src/lib/interfaces/activities.interface.ts
Import handling may also need to recognize the optional field while remaining compatible with older files.
Actual source-code changes belong to Iteration 2 and are not part of this design task.
Design Decision Summary
Stored:
withholdingTax
Derived:
grossDividend
withholdingTaxRate
netDividend
withholdingTaxInAssetProfileCurrency
withholdingTaxInBaseCurrency
The stored withholding amount is the source of truth.
The rate is calculated from the stored amount and gross dividend.
A null value means the withholding information is unknown, while zero means that the system knows no withholding occurred.
This approach keeps the design simple, traceable, backward compatible, and close to Ghostfolio's existing activity structure.
Next Task
The next scheduled tax task is September 17:
Specify the FIFO capital-gains calculation method.
That task will define transaction filtering and ordering, acquisition-lot consumption, partial lot sales, realized gain calculations, traceability, and edge cases.