# Tryton Trade Finance Module — Architecture & Developer Specification

## 1. Objective

This document describes the proposed architecture for a Tryton ERP module dedicated to Trade Finance.

The goal is to support financing that can start at different operational stages of a purchase contract execution:

- purchase contract creation,
- purchase contract line,
- production or goods availability,
- shipment / Bill of Lading,
- transit stage,
- sale matching,
- invoice / receivable stage,
- any future business object that becomes eligible for financing.

The architecture is designed to be evolutive, auditable, and suitable for calculating:

- bank limit usage,
- chronological exposure movements,
- funding cost,
- haircut impact,
- liquidity gain or loss when moving between limits,
- historical exposure at any date.

The core design principle is:

> A bank limit is not a static allocation.  
> It is a balance evolving through chronological movements.

---

## 2. Key Architecture Decision

Use the combination of:

1. **Contextual creation actions**
   - “Start Financing” button available from contract, contract line, shipment, BL, matching, invoice, etc.

2. **A polymorphic origin reference**
   - The financing presentation can reference different Tryton models using a `fields.Reference`.

3. **A movement ledger**
   - Each presentation generates one or more movement lines against bank limits.
   - Current exposure is calculated from the sum of movements, not stored manually.

This makes the module evolutive: adding a new financing start point later should not require redesigning the database schema.

---

## 3. Main Concepts

### 3.1 `bank.limit`

This already exists or is similar in the current system.

It represents the contractual banking facility or available credit line.

Examples:

- UBS PREFI Limit
- UBS In Transit Limit
- BNP Post-Shipment Limit
- Documentary Credit Limit
- Invoice Discounting Facility

Typical fields may include:

```text
bank.limit
- bank
- code
- name
- financing_type
- currency
- maximum_amount
- haircut_percent
- interest_rate
- day_count_basis
- effective_date
- expiration_date
- active
```

If rates and haircuts can change over time, introduce a child table:

```text
bank.limit.condition
- bank_limit
- effective_date
- expiration_date
- interest_rate
- haircut_percent
- margin
- base_rate
```

### 3.2 `trade.finance`

This is the Trade Finance file/header.

It represents the business financing case.

Example:

```text
TFR-0001
Bank: UBS
Bank Reference: UBS/TF/2026/045
Supplier: ABC Brazil
Commodity: Coffee
Currency: USD
```

Important distinction:

- internal reference: generated by Tryton sequence, for example `TFR-0001`
- bank reference: external reference provided by the bank

### 3.3 `trade.finance.presentation`

This is the chronological business event within a Trade Finance file.

Examples:

- initial prefinancing request,
- goods produced,
- transfer to in-transit,
- BL received,
- on-board vessel,
- matched with sale,
- invoice presented,
- financing release,
- cancellation,
- refinancing.

A presentation is not only a form entry. It is the operational event that explains why limit exposure changes.

### 3.4 `trade.finance.presentation.line`

This is the exposure movement ledger.

It behaves like:

- a bank account movement,
- an accounting journal line,
- or a stock movement.

A presentation may generate one or several movement lines.

Example: transfer 400 MT from Prefi Limit to Transit Limit:

```text
Presentation: Transfer 400 MT to in-transit stage

Line 1:
Limit: UBS PREFI
Signed quantity: -400 MT
Signed financed amount: -320,000 USD

Line 2:
Limit: UBS TRANSIT
Signed quantity: +400 MT
Signed financed amount: +360,000 USD
```

---

## 4. Model Responsibilities

### 4.1 `bank.limit`

Responsibility:

```text
bank.limit = contractual banking facility / available line
```

It defines what is theoretically available.

It should answer:

- Which bank owns the limit?
- Which financing type does it support?
- What is the maximum authorized amount?
- What haircut applies?
- What interest rate applies?
- Is the limit active?
- Is the limit valid on the presentation date?
- Which currency applies?

### 4.2 `trade.finance`

Responsibility:

```text
trade.finance = financing file / business case
```

It groups all presentations and movements related to the same financing lifecycle.

It should contain:

```text
trade.finance
- number
- bank_reference
- bank
- party / supplier / counterparty
- company
- currency
- start_date
- status
- description
- presentations
```

### 4.3 `trade.finance.presentation`

Responsibility:

```text
trade.finance.presentation = chronological business event
```

It records the stage or trigger of limit usage.

It should contain:

```text
trade.finance.presentation
- trade_finance
- sequence
- presentation_date
- presentation_type
- finance_stage
- origin
- bank
- description
- previous_presentation
- state
- lines
```

The field `origin` should be a Tryton `fields.Reference`.

Possible origin models:

```text
purchase.contract
purchase.contract.line
stock.shipment
stock.shipment.line
bill.of.lading
sale.purchase.matching
account.invoice
```

New models can be added later to the reference selection.

### 4.4 `trade.finance.presentation.line`

Responsibility:

```text
trade.finance.presentation.line = limit movement / exposure journal
```

It records the signed quantitative and financial impact on a selected bank limit.

It should contain:

```text
trade.finance.presentation.line
- presentation
- bank_limit
- movement_date
- finance_stage
- signed_quantity
- uom
- commodity_price
- gross_collateral_value
- haircut_percent_applied
- financed_amount
- signed_financed_amount
- interest_rate_applied
- day_count_basis_applied
- currency
- maturity_date
- value_date
- description
```

Important design rule:

> Store applied haircut and applied interest rate on the movement line.

Even if these values come from `bank.limit`, copy them at posting time to preserve auditability.

Reason:

```text
bank.limit = what is theoretically available now
presentation.line = what was actually used at that point in time
```

If the bank changes the haircut or rate later, historical calculations must remain correct.

---

## 5. Internal and Bank References

### 5.1 Internal Trade Finance Reference

`trade.finance.number` should be generated by `ir.sequence`.

Example sequence:

```text
TFR-0001
TFR-0002
TFR-0003
```

Suggested field:

```python
number = fields.Char("Trade Finance Reference", required=True, readonly=True)
```

The sequence should be generated during `create()` if no number has been supplied.

### 5.2 Bank Reference

`trade.finance.bank_reference` is manually entered or imported from the bank.

Example:

```text
UBS/TF/2026/045
BNP-COF-18723
```

Suggested field:

```python
bank_reference = fields.Char("Bank Reference")
```

This value is not controlled by Tryton sequence because it comes from the bank.

---

## 6. Financing Type

`financing_type` describes the nature of the bank limit.

Examples:

```text
PREFI
IN_TRANSIT
POST_SHIPMENT
INVENTORY_FINANCING
RECEIVABLE_FINANCING
INVOICE_DISCOUNTING
LETTER_OF_CREDIT
BANK_GUARANTEE
DOCUMENTARY_COLLECTION
FACTORING
```

For an initial version, use a smaller controlled list:

```text
PREFI
IN_TRANSIT
POST_SHIPMENT
MATCHED_SALE
INVOICE_FINANCING
```

Example:

```text
UBS Limit 1
financing_type = PREFI
interest_rate = 10%
haircut = 20%
```

```text
UBS Limit 2
financing_type = IN_TRANSIT
interest_rate = 6%
haircut = 10%
```

The financing type helps filter available limits depending on the stage of contract execution.

---

## 7. Effective Date and Expiration Date on `bank.limit`

### 7.1 `effective_date`

The date from which the bank limit becomes available.

Example:

```text
Limit: UBS PREFI 2026
Effective date: 2026-01-01
Expiration date: 2026-12-31
```

A presentation dated before 2026-01-01 should not normally be allowed to use this limit.

### 7.2 `expiration_date`

The date after which the bank limit is no longer valid.

After this date:

- no new presentation should normally use the limit,
- existing exposure may need to be repaid, renewed, or transferred,
- the limit should be hidden or blocked in normal selection lists.

These dates describe the validity of the credit facility itself.

They are different from:

- contract date,
- shipment date,
- BL date,
- maturity date,
- repayment date,
- value date,
- interest period date.

---

## 8. Finance Stage

Introduce a normalized `finance_stage`.

Examples:

```text
PREFI
PRODUCED
INLAND_TRANSIT
PORT_STOCK
ON_BOARD
OCEAN_TRANSIT
DELIVERED
MATCHED
INVOICED
REPAID
CANCELLED
```

Purpose:

- classify the risk state of the goods,
- determine eligible limits,
- apply haircut and rate rules,
- support reporting,
- support future workflow automation.

A presentation represents a transition or event affecting the finance stage.

---

## 9. Example Scenario

### 9.1 Initial Situation

A purchase contract is signed for 1000 MT.

The bank grants a prefinancing limit.

```text
Bank: UBS
Limit 1: UBS PREFI
Interest rate: 10%
Haircut: 20%
```

The commodity value is:

```text
1000 MT × 1,000 USD/MT = 1,000,000 USD
```

The financed amount is:

```text
1,000,000 × (1 - 20%) = 800,000 USD
```

### 9.2 Presentation 1 — Initial Prefinancing

```text
Presentation #1
Type: PREFI
Origin: Purchase Contract
Quantity: 1000 MT
Limit: UBS PREFI
```

Generated movement line:

```text
+1000 MT on UBS PREFI
+800,000 USD financed amount
```

### 9.3 After 10 Days — 400 MT Move to In-Transit

400 MT have been produced and moved into a lower-risk stage.

New limit:

```text
Limit 2: UBS IN_TRANSIT
Interest rate: 6%
Haircut: 10%
```

For 400 MT:

```text
Gross collateral value = 400 × 1,000 = 400,000 USD
Financed amount under PREFI haircut = 400,000 × 80% = 320,000 USD
Financed amount under IN_TRANSIT haircut = 400,000 × 90% = 360,000 USD
```

Liquidity gain:

```text
360,000 - 320,000 = 40,000 USD
```

### 9.4 Presentation 2 — Transfer to In-Transit

```text
Presentation #2
Type: TRANSFER_TO_IN_TRANSIT
Origin: Shipment / logistics event
Quantity: 400 MT
Previous presentation: Presentation #1
```

Generated movement lines:

```text
Line 1:
Limit: UBS PREFI
Signed quantity: -400 MT
Signed financed amount: -320,000 USD

Line 2:
Limit: UBS IN_TRANSIT
Signed quantity: +400 MT
Signed financed amount: +360,000 USD
```

After this event:

```text
UBS PREFI exposure:
1000 - 400 = 600 MT

UBS IN_TRANSIT exposure:
400 MT
```

The system can now calculate:

- 1000 MT on Limit 1 for 10 days,
- 600 MT on Limit 1 for the following 8 days,
- 400 MT on Limit 2 from the transfer date onward.

---

## 10. Funding Cost Calculation

Funding cost can be calculated from the movement ledger.

Generic formula:

```text
Interest = Exposure × Interest Rate × Days / Day Count Basis
```

Example day count basis:

```text
360
365
ACT/360
ACT/365
```

Because each movement line contains:

- movement date,
- bank limit,
- signed financed amount,
- applied rate,
- day count basis,

the system can reconstruct exposure over time.

### 10.1 Exposure Periods

To calculate interest:

1. order all movement lines by date,
2. calculate running balance per bank limit,
3. identify date intervals between movements,
4. apply the rate to each interval.

Example:

```text
Day 0 to Day 10:
Limit 1 exposure = 800,000 USD

Day 10 to Day 18:
Limit 1 exposure = 480,000 USD
Limit 2 exposure = 360,000 USD
```

### 10.2 Important Rule

Do not overwrite current exposure manually.

Instead:

```text
Current exposure = SUM(signed_financed_amount) GROUP BY bank_limit
```

For reporting performance, a snapshot table can be added later, but it should be derived from ledger lines.

---

## 11. Haircut and Liquidity Gain

Haircut represents the percentage of collateral value that the bank does not finance.

Formula:

```text
Financed Amount = Gross Collateral Value × (1 - Haircut %)
```

When goods move to a lower-risk stage, the haircut may decrease.

Example:

```text
PREFI haircut = 20%
IN_TRANSIT haircut = 10%
Gross value = 400,000 USD
```

Liquidity gain:

```text
400,000 × (20% - 10%) = 40,000 USD
```

This cash gain should be traceable through the presentation lines.

Possible calculated fields:

```text
gross_collateral_value
financed_amount
haircut_amount
liquidity_gain_loss
```

---

## 12. Filtering Bank Limits

When selecting a bank limit in `trade.finance.presentation.line`, the list should be filtered by:

- selected bank from `trade.finance`,
- active status,
- effective date <= presentation date,
- expiration date >= presentation date or expiration date is null,
- currency compatibility,
- financing type / finance stage compatibility.

Conceptual domain:

```python
domain=[
    ('bank', '=', Eval('_parent_presentation', {}).get('bank')),
    ('active', '=', True),
]
```

Additional filtering may be done with `on_change`, dynamic domains, or validation methods depending on the final Tryton model structure.

---

## 13. Evolutivity

The design is evolutive because new start points do not require new foreign keys on `trade.finance`.

Avoid this anti-pattern:

```text
trade.finance
- purchase_contract
- purchase_contract_line
- shipment
- matching
- invoice
- ...
```

This would require schema changes every time a new starting point is added.

Use this instead:

```text
trade.finance.presentation
- origin = fields.Reference(...)
```

Adding a new start point later requires:

1. add the model to the allowed `Reference` selection,
2. implement eligibility logic,
3. add a contextual button/action on the new source object,
4. optionally add presentation defaults for stage, quantity, amount, and origin.

---

## 14. Suggested Tryton Python Skeleton

### 14.1 `trade.finance`

```python
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction


class TradeFinance(ModelSQL, ModelView):
    "Trade Finance"
    __name__ = 'trade.finance'

    number = fields.Char("Trade Finance Reference", required=True, readonly=True)
    bank_reference = fields.Char("Bank Reference")

    bank = fields.Many2One('party.party', "Bank", required=True)
    party = fields.Many2One('party.party', "Counterparty")
    company = fields.Many2One('company.company', "Company", required=True)
    currency = fields.Many2One('currency.currency', "Currency", required=True)

    start_date = fields.Date("Start Date", required=True)
    state = fields.Selection([
        ('draft', "Draft"),
        ('active', "Active"),
        ('closed', "Closed"),
        ('cancelled', "Cancelled"),
    ], "State", required=True, readonly=True)

    presentations = fields.One2Many(
        'trade.finance.presentation', 'trade_finance', "Presentations")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('number', 'DESC'))

    @staticmethod
    def default_state():
        return 'draft'

    @classmethod
    def create(cls, vlist):
        pool = Pool()
        Sequence = pool.get('ir.sequence')

        # Replace this with your module configuration sequence.
        # Recommended: store the sequence on a trade.finance.configuration model.
        for values in vlist:
            if not values.get('number'):
                values['number'] = Sequence.get_id(
                    'trade_finance.sequence_trade_finance')
        return super().create(vlist)
```

### 14.2 `trade.finance.presentation`

```python
class TradeFinancePresentation(ModelSQL, ModelView):
    "Trade Finance Presentation"
    __name__ = 'trade.finance.presentation'

    trade_finance = fields.Many2One(
        'trade.finance', "Trade Finance", required=True, ondelete='CASCADE')

    sequence = fields.Integer("Sequence")
    presentation_date = fields.Date("Presentation Date", required=True)

    presentation_type = fields.Selection([
        ('prefi', "Prefinancing"),
        ('transfer', "Transfer"),
        ('shipment', "Shipment Presentation"),
        ('matching', "Matched Sale"),
        ('invoice', "Invoice Financing"),
        ('release', "Release"),
        ('cancellation', "Cancellation"),
    ], "Presentation Type", required=True)

    finance_stage = fields.Selection([
        ('prefi', "Prefi"),
        ('produced', "Produced"),
        ('inland_transit', "Inland Transit"),
        ('port_stock', "Port Stock"),
        ('on_board', "On Board"),
        ('ocean_transit', "Ocean Transit"),
        ('delivered', "Delivered"),
        ('matched', "Matched"),
        ('invoiced', "Invoiced"),
        ('repaid', "Repaid"),
        ('cancelled', "Cancelled"),
    ], "Finance Stage", required=True)

    origin = fields.Reference(
        "Origin",
        selection='get_origin_models'
    )

    previous_presentation = fields.Many2One(
        'trade.finance.presentation', "Previous Presentation")

    description = fields.Text("Description")

    lines = fields.One2Many(
        'trade.finance.presentation.line', 'presentation', "Movement Lines")

    state = fields.Selection([
        ('draft', "Draft"),
        ('posted', "Posted"),
        ('cancelled', "Cancelled"),
    ], "State", required=True, readonly=True)

    @staticmethod
    def default_state():
        return 'draft'

    @classmethod
    def get_origin_models(cls):
        return [
            ('purchase.contract', "Purchase Contract"),
            ('purchase.contract.line', "Purchase Contract Line"),
            ('stock.shipment', "Shipment"),
            ('sale.purchase.matching', "Sale/Purchase Matching"),
            ('account.invoice', "Invoice"),
        ]
```

### 14.3 `trade.finance.presentation.line`

```python
from decimal import Decimal


class TradeFinancePresentationLine(ModelSQL, ModelView):
    "Trade Finance Presentation Line"
    __name__ = 'trade.finance.presentation.line'

    presentation = fields.Many2One(
        'trade.finance.presentation', "Presentation",
        required=True, ondelete='CASCADE')

    bank_limit = fields.Many2One(
        'bank.limit', "Bank Limit", required=True)

    movement_date = fields.Date("Movement Date", required=True)

    signed_quantity = fields.Numeric("Signed Quantity", digits=(16, 4))
    uom = fields.Many2One('product.uom', "UoM")

    commodity_price = fields.Numeric("Commodity Price", digits=(16, 6))
    gross_collateral_value = fields.Numeric(
        "Gross Collateral Value", digits=(16, 2))

    haircut_percent_applied = fields.Numeric(
        "Haircut % Applied", digits=(5, 2))

    financed_amount = fields.Numeric(
        "Financed Amount", digits=(16, 2))

    signed_financed_amount = fields.Numeric(
        "Signed Financed Amount", digits=(16, 2), required=True)

    interest_rate_applied = fields.Numeric(
        "Interest Rate % Applied", digits=(7, 4))

    day_count_basis_applied = fields.Selection([
        ('360', "360"),
        ('365', "365"),
        ('act_360', "ACT/360"),
        ('act_365', "ACT/365"),
    ], "Day Count Basis")

    currency = fields.Many2One('currency.currency', "Currency", required=True)

    value_date = fields.Date("Value Date")
    maturity_date = fields.Date("Maturity Date")

    description = fields.Text("Description")

    @fields.depends(
        'signed_quantity', 'commodity_price', 'haircut_percent_applied',
        'gross_collateral_value', 'financed_amount',
        'signed_financed_amount')
    def on_change_with_gross_collateral_value(self, name=None):
        if self.signed_quantity is None or self.commodity_price is None:
            return None
        return abs(self.signed_quantity) * self.commodity_price

    @fields.depends(
        'gross_collateral_value', 'haircut_percent_applied')
    def on_change_with_financed_amount(self, name=None):
        if self.gross_collateral_value is None:
            return None
        haircut = self.haircut_percent_applied or Decimal('0')
        return self.gross_collateral_value * (Decimal('1') - haircut / Decimal('100'))
```

---

## 15. Posting Logic

A presentation should probably have a workflow:

```text
draft -> posted -> cancelled
```

When a presentation is posted:

- validate selected limits,
- validate dates,
- validate signed amounts,
- copy applied values from `bank.limit`,
- prevent modification of posted lines,
- update no stored balance directly.

Pseudo-rules:

```text
- Draft presentations can be edited.
- Posted presentations are immutable.
- Cancellation should create reverse movements or mark the presentation as cancelled depending on audit policy.
```

Recommended approach:

> Prefer reversal movements rather than deleting or editing posted movements.

Example cancellation:

```text
Original:
+1000 MT
+800,000 USD

Cancellation:
-1000 MT
-800,000 USD
```

This keeps the audit trail intact.

---

## 16. Reporting and Calculation Views

### 16.1 Current Exposure by Limit

```sql
SELECT
    bank_limit,
    SUM(signed_quantity) AS current_quantity,
    SUM(signed_financed_amount) AS current_financed_amount
FROM trade_finance_presentation_line
GROUP BY bank_limit;
```

### 16.2 Exposure by Trade Finance

```sql
SELECT
    trade_finance,
    bank_limit,
    SUM(signed_financed_amount) AS exposure
FROM trade_finance_presentation_line l
JOIN trade_finance_presentation p
    ON p.id = l.presentation
GROUP BY trade_finance, bank_limit;
```

### 16.3 Historical Exposure

To calculate historical exposure at a date:

```sql
SELECT
    bank_limit,
    SUM(signed_financed_amount) AS exposure
FROM trade_finance_presentation_line
WHERE movement_date <= :as_of_date
GROUP BY bank_limit;
```

### 16.4 Funding Cost

Funding cost requires interval calculation:

```text
For each bank_limit:
1. sort movement lines by movement_date,
2. calculate running exposure,
3. calculate days until next movement,
4. apply interest formula.
```

Formula:

```text
Interest = Exposure × Rate × Days / Day Count Basis
```

---

## 17. UX Suggestions

### 17.1 Contextual Actions

Add a button/action:

```text
Start Financing
```

on:

- purchase contract,
- purchase contract line,
- shipment,
- BL,
- matching,
- invoice.

This button should open a wizard or form with default values:

```text
origin = current record
presentation_type = inferred from source object
finance_stage = inferred from source object
quantity = eligible quantity
bank = selected or inherited
currency = inherited
```

### 17.2 Central Trade Finance Form

The central `trade.finance` form should show:

- header information,
- bank reference,
- internal reference,
- bank,
- counterparty,
- status,
- list of presentations in chronological order,
- current exposure summary.

### 17.3 Presentation Form

The `trade.finance.presentation` form should show:

- business event details,
- origin object,
- previous presentation,
- stage,
- movement lines,
- calculated liquidity impact,
- posting button.

---

## 18. Validation Rules

Suggested validations:

### On `trade.finance`

- number is unique,
- bank reference may be unique per bank if required,
- bank is required,
- currency is required.

### On `trade.finance.presentation`

- presentation date is required,
- origin is required for operational presentations,
- previous presentation is required for transfer events,
- cannot modify posted presentations,
- sequence is chronological within a Trade Finance file.

### On `trade.finance.presentation.line`

- bank limit is required,
- bank limit must belong to the selected bank,
- bank limit must be active,
- bank limit must be valid at movement date,
- currency must match the limit or be explicitly converted,
- signed amount cannot be zero,
- applied rate and haircut must be set before posting,
- posted lines are immutable.

---

## 19. Design Rules to Preserve

1. Do not store one foreign key per possible start point on `trade.finance`.
2. Use `fields.Reference` on `trade.finance.presentation.origin`.
3. Treat `presentation.line` as a journal/ledger.
4. Store signed movement values.
5. Never overwrite exposure balances manually.
6. Store applied rate and haircut on movement lines for auditability.
7. Use reversal movements for cancellations.
8. Use snapshots only as derived reporting tables, not as source of truth.
9. Filter available bank limits by bank, date, status, currency, and finance stage.
10. Keep the architecture open to future financing start points.

---

## 20. Suggested Module Files

Possible Tryton module structure:

```text
trade_finance/
├── __init__.py
├── __manifest__.py
├── trade_finance.py
├── bank.py
├── configuration.py
├── trade_finance.xml
├── bank.xml
├── configuration.xml
├── message.xml
├── tests/
│   ├── __init__.py
│   └── test_trade_finance.py
```

Possible models:

```text
trade.finance
trade.finance.presentation
trade.finance.presentation.line
trade.finance.configuration
trade.finance.configuration.sequence
```

If `bank.limit` already exists, extend it only if necessary.

---

## 21. Initial Development Roadmap

### Phase 1 — Core Models

- Create `trade.finance`
- Create `trade.finance.presentation`
- Create `trade.finance.presentation.line`
- Add sequence for `TFR-0001`
- Link presentation lines to existing `bank.limit`

### Phase 2 — Basic UX

- Create menu entries
- Create list/form views
- Add contextual action from purchase contract
- Add contextual action from purchase contract line
- Add contextual action from shipment

### Phase 3 — Posting and Validation

- Add draft/posted/cancelled states
- Block edits after posting
- Validate bank limit eligibility
- Copy applied haircut and rate on posting

### Phase 4 — Exposure Reporting

- Current exposure by bank limit
- Exposure by Trade Finance
- Historical exposure as of date
- Presentation chronological view

### Phase 5 — Funding Cost and Haircut Engine

- Calculate exposure periods
- Calculate funding cost
- Calculate haircut effect
- Calculate liquidity gain/loss during transfers

### Phase 6 — Advanced Workflow

- Add matching with sale
- Add invoice financing
- Add repayment/release
- Add automatic transfer proposals
- Add alerts for expired limits or over-limit exposure

---

## 22. Final Conceptual Summary

The final architecture is:

```text
bank.limit
    = available contractual bank facility

trade.finance
    = financing file / internal TFR reference

trade.finance.presentation
    = chronological business event in the financing lifecycle

trade.finance.presentation.line
    = signed movement against a bank limit
```

The system behaves like a Trade Finance subledger.

It allows the business to answer:

- Which limit is currently used?
- What is the exposure by limit?
- What was the exposure at a past date?
- Which financing stage generated the exposure?
- How much funding cost has been incurred?
- What cash/liquidity gain was generated by moving to a lower-risk limit?
- Which operational document triggered each movement?

This architecture is evolutive, auditable, and suitable for complex commodity trade finance workflows.
