|
|
@ -165,4 +165,280 @@ The future Iteration 2 implementation is expected to extend the existing |
|
|
`Order` model with: |
|
|
`Order` model with: |
|
|
|
|
|
|
|
|
```prisma |
|
|
```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. |
|
|
|