# Mapping Rules: CoA, Contacts, Tax, Currencies

## Chart of Accounts Mapping

### Fresh Org Strategy (Replace)

When the Jaz org has only default system-generated accounts:

1. **Discover system accounts:** `GET /api/v1/chart-of-accounts/search`; system-generated accounts have `isSystemGenerated: true`. These cannot be deleted.
2. **Skip accounts that already exist** (matched by name or code from probe)
3. **Bulk-upsert source accounts** using `POST /api/v1/chart-of-accounts/bulk-upsert` with `{ accounts: [...] }` wrapper
4. **Match by code first, then name**: if a system account has the same code as a source account, map to it

### Existing Org Strategy (Match)

When the Jaz org has user-customized accounts:

1. **GET all existing accounts** from Jaz
2. **3-tier fuzzy matching** (reuse seeder's proven approach):
   - **Tier 1. Exact name match:** Case-insensitive exact match on account name
   - **Tier 2. Fuzzy match (Jaro-Winkler):** JW distance > 0.85 between names
   - **Tier 3. Token overlap:** Split names into tokens, overlap > 60%
3. **Code as tiebreaker:** If multiple name matches, prefer the one with matching account code
4. **Create missing accounts** for any unmatched source accounts

### Account Type Mapping

Source systems use different classification names. Map to Jaz types:

| Source (common names) | Jaz `classificationType` |
|----------------------|-------------------------|
| Cash, Bank | Bank Accounts |
| Accounts Receivable, Trade Debtors | Current Asset |
| Inventory, Stock | Inventory |
| Prepaid Expenses | Current Asset |
| Fixed Assets, Property/Equipment | Fixed Asset |
| Accumulated Depreciation | Fixed Asset |
| Accounts Payable, Trade Creditors | Current Liability |
| Accrued Liabilities | Current Liability |
| Loans (long-term) | Non-current Liability |
| Owner's Equity, Share Capital | Shareholders Equity |
| Retained Earnings | Shareholders Equity |
| Sales, Revenue, Income | Operating Revenue |
| Cost of Goods Sold, COGS | Direct Costs |
| Operating Expenses | Operating Expense |
| Other Income | Other Revenue |

**Valid `classificationType` values (exactly 23):** Bank Accounts, Cash, Current Asset, Non-current Asset, Fixed Asset, Inventory, Investment, Goodwill, Current Liability, Non-current Liability, Shareholders Equity, Operating Revenue, Other Revenue, Discontinued Income, Financing Income, Investing Income, Direct Costs, Operating Expense, Other Expense, Finance Cost, Investing Expense, Income Tax Expense, Discontinued Expense

The set is GLOBAL (identical for every org). Confirm with `list_account_classifications` using `limit: 100`; its default page size is 20 and there are 23 types.

### Clearing Account Creation

For Quick Conversion, create two clearing accounts:

```json
{
  "accounts": [
    {
      "name": "AR Conversion Clearing",
      "code": "1299",
      "classificationType": "Current Asset",
      "description": "Contra account for conversion AR balances, should net to zero"
    },
    {
      "name": "AP Conversion Clearing",
      "code": "2199",
      "classificationType": "Current Liability",
      "description": "Contra account for conversion AP balances, should net to zero"
    }
  ]
}
```

Choose codes that don't conflict with existing accounts. Adjust `1299`/`2199` if those codes are taken.

### Identifying AR and AP Accounts in the Trial Balance

The TTB journal must know which TB accounts are Accounts Receivable vs Accounts Payable, because in Quick conversion, those balances get routed through clearing accounts instead of posted directly (to avoid doubling with the conversion invoices/bills).

Identify AR/AP control accounts by name (case-insensitive):
- **AR:** "Accounts Receivable", "Trade Debtors", "Trade Receivables"
- **AP:** "Accounts Payable", "Trade Creditors", "Trade Payables"

If the source CoA uses different names (e.g., "Debtors Control", "Sundry Creditors"), the mapping step must tag which accounts are AR/AP control accounts so the TTB journal routes them correctly.

## Contact Mapping

### Matching Rules
1. **Exact name match** (case-insensitive)
2. **Fuzzy name match** (Jaro-Winkler > 0.85): handles minor variations like "Pte Ltd" vs "Pte. Ltd."
3. **If no match → create new contact**

### Filtering Noise from Aging Reports

AR/AP aging reports from source systems mix real contact names with garbage rows. Before creating contacts from an aging report, reject any string that is:

- **A date**: `YYYY-MM-DD`, `DD/MM/YYYY`, day names ("Monday"), or phrases like "as at", "as of"
- **A total label**: "Total", "Sub-total", "Grand Total", "Percentage", or anything ending in "Total"
- **A report or column header**: "aging", "current", "91+ days", "doc. date", "balance", "amount"
- **Purely numeric**: strings with no letters at all (e.g., "1,234.56", "$100", "0.00")
- **Shorter than 2 characters**: single letters or empty strings

Every aging report has these noise rows: subtotal lines, aging bucket headers, report metadata. Creating contacts from them produces junk data that must be manually cleaned up.

### Contact Types
Jaz uses boolean flags, NOT an enum:
- Source "Customer" → `customer: true, supplier: false`
- Source "Supplier" / "Vendor" → `customer: false, supplier: true`
- If both (customer AND supplier) → `customer: true, supplier: true`

### Required Fields
- `name`: the contact's business name
- `billingName`: what shows on invoices/bills (often same as name)
- `customer`: boolean (true if the contact is a customer)
- `supplier`: boolean (true if the contact is a supplier)

### Optional but Recommended
- `email`: for payment reminders
- `phone`: **MUST be E.164** if provided (`+65XXXXXXXX` for SG, `+63XXXXXXXXXX` for PH)
- `currency`: default currency for this contact (for FX contacts)

### Contact API Example
```json
POST /api/v1/contacts
{
  "name": "Acme Corp",
  "billingName": "Acme Corp",
  "customer": true,
  "supplier": false,
  "email": "billing@acme.com"
}
```

## Items/Products Mapping

### Detection
The items extractor detects columns for: name, code, description, type (PRODUCT/SERVICE), sale price, sale account, sale tax profile, purchase price, purchase account, purchase tax profile.

### Item Types
- `PRODUCT`: physical goods (default if type column not present)
- `SERVICE`: services

### Payload Shape
```json
POST /api/v1/items
{
  "internalName": "Widget Pro",
  "itemCode": "WGT-001",
  "type": "PRODUCT",
  "appliesToSale": true,
  "saleItemName": "Widget Pro",
  "salePrice": 99.00,
  "saleAccountResourceId": "<revenue account ID>",
  "saleTaxProfileResourceId": "<SR tax profile ID>",
  "appliesToPurchase": true,
  "purchaseItemName": "Widget Pro",
  "purchasePrice": 65.00,
  "purchaseAccountResourceId": "<COGS account ID>",
  "purchaseTaxProfileResourceId": "<TX tax profile ID>"
}
```

### Key Rules
- Items have **two independent sides**: sale and purchase. Each side has its own account and tax profile.
- Set `appliesToSale: true` if the item has sale pricing/account. Set `appliesToPurchase: true` if it has purchase pricing/account.
- Account and tax profile references use template strings (`{{account:Revenue}}`, `{{taxProfile:SR}}`) resolved at execution time via the probe.
- If the source doesn't distinguish sale vs purchase accounts, use the same account for both.

## Tax Profile Mapping

**Tax profiles are READ-ONLY in Jaz.** They are pre-provisioned per org. You can only discover and map.

### Discovery
```
GET /api/v1/tax-profiles
```
Returns all available tax profiles with `taxTypeCode`, `taxPercentage`, and `description`.

### Common Mappings

**Singapore (GST):**
| Source Tax Code | Jaz taxTypeCode | Rate |
|----------------|-----------------|------|
| GST 9%, Standard Rate | SR | 9% |
| GST on Purchases | TX | 9% |
| Zero Rated | ZR | 0% |
| Exempt | ES | 0% |
| Out of Scope | OS | 0% |
| No Tax | - | Skip tax on line item |

**Philippines (VAT):**
| Source Tax Code | Jaz taxTypeCode | Rate |
|----------------|-----------------|------|
| 12% VAT | S12 | 12% |
| VAT Exempt | VE | 0% |
| Zero Rated | VZ | 0% |

### Exempt Tax Profile Split (Sales vs Purchase)
Jaz has separate exempt tax profiles for sales and purchases. Use the correct one:
- **Invoices / Customer Credit Notes** → exempt profile where `appliesToSale !== false`
- **Bills / Supplier Credit Notes** → exempt profile where `appliesToPurchase !== false`

The probe discovers both: `exemptSalesTaxProfileId` and `exemptPurchaseTaxProfileId`. Using the wrong one may cause validation errors.

### Handling "No Tax" / Tax-Free Items
If a source line item has no tax, omit `taxProfileResourceId` from the line item payload entirely; do NOT set it to a zero-rate profile unless the source explicitly used one.

## Currency Mapping

### Enable Currencies
Before creating any FX transactions or CoA entries with foreign currency:

```
POST /api/v1/organization/currencies
{
  "currencies": ["USD", "EUR", "JPY"]
}
```

**Note:** This is a POST (not PUT).

### Set FYE Exchange Rates
For Quick Conversion, all conversion transactions use the FYE closing rate. Rates MUST be set AFTER currencies are enabled:

```
POST /api/v1/organization/currencies/<code>/rates
{
  "rate": 0.74,
  "rateApplicableFrom": "2023-12-31"
}
```

**Rate direction:** `functionalToSource`, how many units of SOURCE (foreign) currency = 1 unit of FUNCTIONAL (base) currency. Example: base SGD, 1 SGD = 0.74 USD → rate = 0.74. A quote written foreign-first ("1 USD = 1.35 SGD") is the inverse: send 1/1.35 = 0.74, or send 1.35 with `rateDirection: "SOURCE_TO_FUNCTIONAL"` and let the endpoint apply it. Omitting `rateDirection` means `FUNCTIONAL_TO_SOURCE`.

**Note:** Rate endpoints and enable/disable both live under the nested `/organization/currencies` family. The older hyphenated `/organization-currencies/...` rate paths still resolve but are **superseded**; use the nested form.

**CRITICAL:** The field is `rateApplicableFrom` (NOT `effectiveDate`). Using the wrong field name will silently fail.

## Confidence Scoring

For each mapping, assign a confidence level:

| Confidence | Criteria | Action |
|-----------|---------|--------|
| **High** | Exact match (name or code) | Auto-accept |
| **Medium** | Fuzzy match (JW > 0.85 or token > 60%) | Flag for review |
| **Low** | No match found, creating new | Flag for review |
| **Conflict** | Multiple possible matches | Must resolve manually |

Present all medium, low, and conflict mappings to the human reviewer before proceeding.
