Browse Source

docs(tax): complete withholding tax schema

pull/7897/head
Tharun Ravi Kumar 4 weeks ago
parent
commit
25846ec6c2
  1. 278
      project-docs/iteration-1/tax/withholding-tax-schema.md

278
project-docs/iteration-1/tax/withholding-tax-schema.md

@ -165,4 +165,280 @@ The future Iteration 2 implementation is expected to extend the existing
`Order` model with:
```prisma
withholdingTax Float?
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:
1. `withholdingTax` is optional.
2. If provided, `withholdingTax` must be numeric.
3. The value must be greater than or equal to zero.
4. Withholding tax is only valid for activities where `type = DIVIDEND`.
5. For a valid dividend, `withholdingTax <= grossDividend`.
6. A negative withholding amount is invalid.
7. A positive withholding amount on a zero-value dividend is invalid.
8. If an activity containing withholding tax is changed from `DIVIDEND` to
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:
- `withholdingTaxInAssetProfileCurrency`
- `withholdingTaxInBaseCurrency`
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
```ts
@IsNumber()
@Min(0)
@IsOptional()
withholdingTax?: number | null;
```
### UpdateOrderDto
```ts
@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:
- `withholdingTaxRate`
- `withholdingTaxInAssetProfileCurrency`
- `withholdingTaxInBaseCurrency`
- `netDividend`
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
```text
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
fee = 0
withholdingTax = 15.00
```
```text
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
```text
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
withholdingTax = 0
```
```text
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
```text
type = DIVIDEND
quantity = 10
unitPrice = 10.00
currency = USD
withholdingTax = null
```
```text
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 `DIVIDEND` to 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.prisma`
- `libs/common/src/lib/dtos/create-order.dto.ts`
- `libs/common/src/lib/dtos/update-order.dto.ts`
- `apps/api/src/app/activities/activities.service.ts`
- `libs/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
```text
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.

Loading…
Cancel
Save