# LabaBersih v1 — Integration & Business Logic Reference

> Dokumen ini merekam SEMUA integrasi dan business logic yang berhasil diimplementasi di v1.
> Tujuan: referensi untuk membangun v2 (Elixir/Phoenix) tanpa harus bongkar git history.
> Generated: 31 Maret 2026

---

## Daftar Isi

1. [TikTok Shop](#1-tiktok-shop)
2. [Mengantar (Kurir COD)](#2-mengantar)
3. [ScaleV (B2B Order Management)](#3-scalev)
4. [TikTok Ads](#4-tiktok-ads)
5. [Facebook/Meta Ads](#5-facebookmeta-ads)
6. [Google Ads](#6-google-ads)
7. [Auto Journal Engine (Accounting)](#7-auto-journal-engine)
8. [FIFO HPP Engine (Inventory Costing)](#8-fifo-hpp-engine)
9. [Closing Engine (Period Closing)](#9-closing-engine)
10. [XLSX Parsers](#10-xlsx-parsers)
11. [Supabase RPC Functions](#11-supabase-rpc-functions)
12. [Edge Functions (Complete List)](#12-edge-functions)
13. [Database Tables](#13-database-tables)
14. [Environment Variables](#14-environment-variables)
15. [Resend Email](#15-resend-email)
16. [Sentry Error Tracking](#16-sentry-error-tracking)
17. [Cron Jobs (pg_cron)](#17-cron-jobs)
18. [Critical Business Rules](#18-critical-business-rules)
19. [Key Gotchas & Lessons Learned](#19-key-gotchas--lessons-learned)

---

## 1. TikTok Shop

### OAuth Flow
- **Edge Function:** `tiktok-shop-callback`
- User diarahkan ke TikTok OAuth → redirect ke callback dengan `code`
- State param berisi `org_id` (base64-encoded JSON)
- Exchange code → access_token + refresh_token
- Ambil authorized shops via `getAuthorizedShops()`
- Upsert ke `tiktok_shop_integrations` (onConflict: `org_id,open_id`)
- Redirect ke `/pengaturan/integrasi/tiktok-shop?connected=true`

### API Signing (HMAC-SHA256)
```
signature = HMAC-SHA256(
  key: app_secret,
  message: app_secret + path + sorted_params + body_json + app_secret
)
```
- **WAJIB:** Params sorted alphabetically by key
- **WAJIB:** Exclude `sign`, `access_token`, `x-tts-access-token` dari signature
- Add `sign` param SETELAH compute signature

### Token Management
- Proactive refresh 2 jam sebelum expired (`REFRESH_BUFFER_MS = 2 * 60 * 60 * 1000`)
- `getValidAccessToken()` cek expiry → refresh kalau perlu → update DB
- Kalau refresh gagal di cron → deactivate integration (`is_active = false`)

### Order Sync
- **Edge Function:** `tiktok-shop-sync`
- Filter: `order_status = AWAITING_COLLECTION` (default)
- Pagination: max 10 pages × 100 orders/page
- Detail batch: 20 orders per `getOrderDetail()` call
- Upsert ke `tiktok_shop_orders` staging table
- **Action routing:** `list_integrations`, `update_store`, `delete_integration` via `action` field

### Order Mapper (`tiktok-shop-mapper.ts`)
- **Formula:** `totalHarga = payment.sub_total` (SUDAH after discount)
- **JANGAN:** `sub_total - seller_discount` → double subtraction bug!
- Status mapping: AWAITING_SHIPMENT → `dibuat`, AWAITING_COLLECTION → `dipacking`
- IN_TRANSIT/DELIVERED/COMPLETED/CANCELLED → skip
- Proportional distribution: distribute totalHarga ke items, last item gets remainder

### RTS Sync
- **Edge Function:** `tiktok-shop-sync-rts`
- Cari CANCELLED orders → filter by cancel_reason (case-insensitive substring match)
- Patterns: "package delivery failed", "package rejected", "package damaged", "package lost", "package scrapped", "late delivery"
- Upsert ke `tiktok_shop_returns` staging
- Patch grossAmount: `originalPrice - sellerDiscount` (fallback: refund_total)
- Max 500 orders per sync

### Settlement Sync (Finance API)
- **Edge Function:** `tiktok-shop-sync-settlement`
- **H+1 Rule:** API `statement_time` = H, XLSX = H+1. Shift date -1 hari saat query.
- Flow: `getStatements()` → per statement `getStatementTransactions()` → filter type=ORDER
- Fee field mapping (9 known fees):

| API Field | Label | Akun |
|---|---|---|
| `platform_commission_amount` | Platform commission fee | Biaya admin platform |
| `dynamic_commission_amount` | Dynamic commission | Biaya komisi dinamis |
| `bonus_cashback_service_fee_amount` | Bonus cashback service fee | Biaya cashback |
| `transaction_fee_amount` | Payment Fee | Biaya transaksi |
| `affiliate_commission_amount` | Affiliate Commission | Biaya affiliate |
| `affiliate_partner_commission_amount` | Affiliate partner commission | Biaya affiliate |
| `sfp_service_fee_amount` | SFP service fee | Biaya SFP |
| `affiliate_ads_commission_amount` | Affiliate Commission | Biaya affiliate |
| `vn_fix_infrastructure_fee` | Order processing fee | Biaya proses pesanan |

- Fee yang gak dikenal → humanize field name → user pilih akun manual
- Upsert ke `tiktok_shop_settlements` in 100-item batches

### Ship Package
- **Edge Function:** `tiktok-shop-ship-package`
- Call Fulfillment API: `packages/{packageId}/ship` dengan `handover_method: "PICKUP"`
- **KRITIS:** Tunggu 3 detik setelah ship → baru fetch tracking_number
- Re-fetch order detail untuk dapat tracking number

### Print Label
- **Edge Function:** `tiktok-shop-print-label`
- Fetch shipping document dari `packages/{packageId}/shipping_documents`
- Document type: `SHIPPING_LABEL_AND_PACKING_SLIP`, size: A6
- Pakai `pdf-lib` untuk handle rotation (landscape → portrait)
- Return PDF inline

### Inventory Sync
- **Edge Function:** `tiktok-shop-inventory`
- Action "preview": compare local stock vs TikTok stock
- Action "push": update TikTok inventory per productId
- Batch: 10 SKUs per API call (API limit)

### Webhook
- **Edge Function:** `tiktok-shop-webhook` (placeholder 200 OK)
- Signature verification ready: `verifyWebhookSignature()` pakai `crypto.timingSafeEqual()`
- Actual handler belum implement (placeholder)

### Cron
- **Edge Function:** `tiktok-shop-cron`
- Auto-sync orders (max 5 pages) + RTS (max 300)
- Respects `auto_sync_orders` dan `auto_sync_rts` flags
- Default window: 3 hari ke belakang (WIB)

### Environment Variables
```
TIKTOK_SHOP_APP_KEY
TIKTOK_SHOP_APP_SECRET
TIKTOK_SHOP_AUTH_ENDPOINT
TIKTOK_SHOP_BASE_URL
TIKTOK_SHOP_WEBHOOK_SECRET
```

### Database Tables
- `tiktok_shop_integrations` — OAuth tokens, shops, config
- `tiktok_shop_orders` — staging (raw order data)
- `tiktok_shop_returns` — staging (RTS data)
- `tiktok_shop_settlements` — staging (settlement/fee data)

---

## 2. Mengantar

### API Client (`mengantar-api.ts`)
- Base URL: `https://api-public.mengantar.com/api/public/{API_KEY}/...`
- Auth: API key embedded in URL path (bukan header)
- Rate limit: retry on 429 with `retry-after` header
- Timeout: 30 detik per request
- Auto-pagination: max 50 pages safety limit

### Order Sync
- **Edge Function:** `mengantar-sync`
- Fetch semua status (user filter di preview) — BEDA dari TikTok yang filter di API
- Upsert ke `mengantar_orders` staging in batches of 50
- On batch failure → retry one-by-one (fallback)

### RTS Sync
- **KRITIS:** Filter by `lastStatusChange` (kapan status jadi RTS), BUKAN order creation date
- Status regex: `/RTS|SHIPMENT.?RETURN|RETURNED/`
- Upsert ke `mengantar_returns` staging
- Patch grossAmount ke `return_records` where null or 0

### Revenue Formula
| Tipe | Formula |
|---|---|
| **COD** | `totalHarga = productValue - estimatedPrice` (ongkir sudah dipotong settlement) |
| **Non-COD (transfer)** | `totalHarga = productValue` |

### Status Mapping
- PICKUP, PENDING_PICKUP, ACTIVE, CREATED, PENDING, NEW → `dipacking`
- ON_PROCESS, IN_TRANSIT, ON_DELIVERY, MANIFEST → `dikirim`
- DELIVERED, COLLECTED → `selesai`
- RTS, SHIPMENT_RETURN, RETURNED → `rts`
- **Semua status masuk staging** — user decide di preview

### Settlement
- **Edge Function:** `mengantar-settlement`
- Data dari `mengantar_settlements` raw table (pre-fetched by cron)
- **Gak call API** — return empty kalau cron belum jalan
- Fee details: cod_fee + shipping_fee saja

### Environment Variables
```
MENGANTAR_API_KEY (format: API-...)
```

### Database Tables
- `mengantar_integrations` — API key per org/store
- `mengantar_orders` — staging
- `mengantar_returns` — staging
- `mengantar_settlements` — settlement staging

---

## 3. ScaleV

### Webhook (`scalev-webhook`)
- HMAC-SHA256 signature verification
- Rate limit: 120 req/min per IP
- Upsert order data ke `scalev_orders`
- Signing secret per org dari `scalev_integrations.webhook_secret`

### Auto-Import Cron (`cron-sync-scalev`)
- Query `scalev_orders` where status=confirmed AND synced_to_orders=false
- SKU matching via `matchOrderItems()` (4-level: exact SKU → exact name → SKU in name → contains)
- Match score 100% → auto-create order via `insertBulkOrders()`
- Match score < 100% → `sync_queue` with status=needs_review
- Dedup: check `sync_queue` + `orders` table sebelum insert
- Order ID format: `PS{YYMM}-{NNNNN}` (sequential per bulan)

### Order Mapper (`scalev-order-mapper.ts`)
- **Formula:** `totalHarga = product_price` (BUKAN gross_revenue!)
- `gross_revenue = product_price + shipping_cost - shipping_discount` (reference only)
- Platform: always `"mengantar"` (ScaleV = B2B channel ke Mengantar)
- Address: primary dari `raw_payload.destination_address`, fallback ke table columns

### Courier Normalization
```
jne → JNE, j&t/jnt → J&T, sicepat → SiCepat, anteraja → AnterAja,
ninja → Ninja, pos → POS, lion → Lion Parcel, idx/id express → ID Express,
spx/shopee → SPX, tiki → TIKI, grab → Grab, gojek/gosend → GoSend
```

### Environment Variables
```
SCALEV_SIGNING_SECRET (per org, stored in scalev_integrations)
```

### Database Tables
- `scalev_integrations` — webhook config, auto_sync flag
- `scalev_orders` — raw webhook data
- `sync_queue` — SKU match results, auto_imported/needs_review

---

## 4. TikTok Ads

### OAuth
- **Edge Function:** `tiktok-ads-callback`
- Exchange auth code → access_token + refresh_token + advertiser_ids
- Store di `tiktok_ad_integrations`

### Cron Sync (`cron-sync-tiktok-ads`)
- Schedule: `:05` setiap jam
- Date range: 3 hari ke belakang (late-reported spend)
- Auto-refresh token 1 jam sebelum expired
- **Deactivate integration** kalau refresh gagal

### Two Report Types

**1. Web Conversion (Standard Ads)**
- API: `report/integrated/get/` dengan `report_type=BASIC`, `data_level=AUCTION_ADVERTISER`
- Metrics: spend, impressions, clicks, cpc, cpm, ctr
- Channel: `tiktok_webconv`
- Table: `tiktok_ad_reports`

**2. GMV Max (Shop Ads)**
- API: `gmv_max/report/get/` (dedicated endpoint)
- 4-step multi-level fetch:
  1. Campaign level (spend, orders, gross_revenue, roi)
  2. Product level (per item_group_id)
  3. Creative level (video metrics, product impressions)
  4. Creative attributes (tt_account_name, authorization_type)
- Channel: `tiktok_ads`
- Table: `tiktok_ad_reports` (same table, different report_level)

### Database Tables
- `tiktok_ad_integrations` — OAuth tokens, advertiser_id
- `tiktok_ad_reports` — daily ad metrics (upsert on: org_id, advertiser_id, report_level, campaign_id, item_group_id, item_id, date)

### Environment Variables
```
TIKTOK_ADS_APP_ID
TIKTOK_ADS_SECRET
```

---

## 5. Facebook/Meta Ads

### Cron Sync (`cron-sync-facebook`)
- Schedule: `:00` setiap jam
- API: Facebook Graph API v22.0 (`/act_{id}/insights`)
- Level: `ad` (granular per ad)
- Date range: 3 hari ke belakang
- Pagination: cursor-based via `paging.next`

### Metrics Tracked
```
spend, impressions, clicks, cpc, cpm, ctr, reach, frequency,
outbound_clicks, landing_page_views, content_views,
add_to_cart, add_payment_info, initiate_checkout,
purchases, purchase_value, leads, cost_per_lead,
messaging_conversations_started, link_clicks
```

### Action Value Extraction
- Facebook returns actions as array: `[{ action_type: "omni_purchase", value: "5" }]`
- Helper `getActionValue()` finds by action_type string
- Key types: `omni_purchase`, `omni_view_content`, `omni_add_to_cart`, `landing_page_view`, `lead`, `link_click`

### Database Tables
- `fb_ad_integrations` — ad_account_id, access_token, channel
- `fb_ad_reports` — daily ad metrics per ad (upsert on: org_id, store_id, date, channel, sku, ad_id)

### Environment Variables
```
(stored per integration in fb_ad_integrations: access_token, ad_account_id)
```

---

## 6. Google Ads

### Architecture: Apps Script → Edge Function
- **Bukan** pakai Google Ads API langsung (pending Basic Access approval)
- Apps Script jalan di Google Ads account, push data ke Supabase
- Script copy dari halaman integrasi (token auto-embed)

### Edge Function (`google-ads-sync-direct`)
- Receive POST dari Apps Script
- Auth via `x-sync-token` header (match ke `google_ad_integrations.sync_token`)
- Data: daily ad rows + conversion breakdown
- Date normalization: `YYYYMMDD` → `YYYY-MM-DD`
- Upsert ke `ad_entries` table

### Database Tables
- `google_ad_integrations` — sync_token, account_id
- `ad_entries` — daily ad metrics

---

## 7. Auto Journal Engine

**File:** `src/lib/auto-journal-engine.ts`

### Fee Calculation
```typescript
calculateEstimatedFees(order, store) → { gross, fees[], totalFees, netPiutang }
```
- Per fee in `store.feeMapping`: flat amount or `Math.round(gross × percent / 100)`
- Each fee rounded independently
- `netPiutang = gross - totalFees`

### 13 Journal Types

#### 1. Sale (`generateSaleJournal`)
```
Dr. Piutang {platform}          NET
Dr. Biaya Layanan                fee1
Dr. Biaya Admin                  fee2
Dr. Biaya Proses                 fee3
    Cr. Pendapatan {platform}    GROSS
```
Optional HPP: `Dr. HPP / Cr. Persediaan`
Special: Mengantar non-COD → additional `Dr. Beban Ongkir / Cr. Kas`
Special: Sample (totalHarga=0) → `Dr. Beban Sampel / Cr. Persediaan`
Date: `shippedAt` (revenue recognition)

#### 2. Reconciliation (`generateReconciliationJournal`)
- **Match:** `Dr. Kas / Cr. Piutang`
- **Over:** `Dr. Kas / Cr. Piutang + Cr. Adjustment`
- **Under:** `Dr. Kas + Dr. Adjustment / Cr. Piutang`

#### 3. RTS (`generateRtsJournal`)
```
Dr. Retur Penjualan              GROSS
    Cr. Piutang {platform}       NET
    Cr. Biaya Layanan            fee1 (reversal)
    Cr. Biaya Admin              fee2 (reversal)
```
Optional: `Dr. Persediaan / Cr. HPP` (stock reversal)

#### 4. RTS Inbound Rusak (`generateRtsInboundRusakJournal`)
```
Dr. Beban Kerugian Barang Rusak  hppTotal
    Cr. Persediaan               hppTotal
```

#### 5. RTS Inbound Hilang (`generateRtsInboundHilangJournal`)
```
Dr. Piutang Klaim Kurir          hppTotal
    Cr. Persediaan               hppTotal
```

#### 6. Claim Accepted (`generateClaimAcceptedJournal`)
```
Dr. Kas                          amount
    Cr. Piutang Klaim Kurir      amount
```

#### 7. Claim Rejected (`generateClaimRejectedJournal`)
```
Dr. Beban Kerugian Piutang       amount
    Cr. Piutang Klaim Kurir      amount
```

#### 8. PO Receive (`generatePOReceiveJournal`)
- Cash PO: `Dr. Persediaan / Cr. Kas`
- Credit PO: `Dr. Persediaan / Cr. Hutang Usaha`
- Amount includes tax: `subtotal × (1 + taxPercent/100)`

#### 9. PO Payment (`generatePOPaymentJournal`)
```
Dr. Hutang Usaha                 amount
    Cr. Kas                      amount
```

#### 10. Standalone RTS (`generateStandaloneRtsJournal`)
- Sama pattern dengan RTS, tapi untuk order yang GAK ADA di database
- Reduce piutang saldo awal
- Mengantar: feeMapping kosong → simple Dr. Retur / Cr. Piutang

#### 11. PO Return (`generatePOReturnJournal`)
- Cash PO: `Dr. Kas / Cr. Persediaan`
- Credit PO: `Dr. Hutang Usaha / Cr. Persediaan`

#### 12. HPP Adjustment (`generateHppAdjustmentJournal`)
```
Dr. HPP                          hppTotal
    Cr. Persediaan               hppTotal
```
Date: retroactive (`shippedAt` — for correct accounting period)

### Account Code Constants (ACCT)
| Constant | Code | Name |
|---|---|---|
| KAS | 11-100 | Kas |
| PIUTANG_KLAIM_KURIR | 12-400 | Piutang Klaim Kurir |
| PERSEDIAAN_BARANG_DAGANG | 13-100 | Persediaan Barang Dagang |
| HUTANG_USAHA | 20-100 | Hutang Usaha |
| LABA_DITAHAN | 30-300 | Laba Ditahan |
| LABA_TAHUN_BERJALAN | 30-500 | Laba Tahun Berjalan |
| IKHTISAR_LABA_RUGI | 30-400 | Ikhtisar Laba Rugi |
| RETUR_PENJUALAN | 41-100 | Retur Penjualan |
| HPP | 50-100 | Harga Pokok Penjualan |
| BEBAN_SAMPEL_PRODUK | varies | Beban Sampel Produk |
| BEBAN_ONGKIR_NON_COD | varies | Beban Ongkir Non-COD |
| BEBAN_KERUGIAN_PIUTANG | 64-600 | Beban Kerugian Piutang |
| BEBAN_KERUGIAN_BARANG_RUSAK | 64-700 | Beban Kerugian Barang Rusak |

### Key Rules
1. Only fees whose accounts exist are journaled; missing → skipped silently
2. `netPiutang = GROSS - only_journaled_fees` (ensures balance)
3. Date selection: Sale=shippedAt, Reconciliation=settlementDate, RTS/Claim/PO=now, HPP Adj=shippedAt (retroactive)
4. Returns null if amount ≤ 0

---

## 8. FIFO HPP Engine

**File:** `src/lib/fifo-engine.ts`

### Sort Order
1. **FEFO** — `expires_at ASC NULLS LAST` (expired first)
2. **FIFO tiebreaker** — `created_at ASC`

### Key Functions
- `consumeProductFifo(productId, qty, lots, orderId)` → `{ hppTotal, consumptions[], updatedLots[] }`
- `consumeLotsForOrder(items, productMap, lots, orderId)` → handles bundles + regular products
- `createLotFromPO(productId, qty, unitCost, poId, receiveRecordId, expiresAt)` → new lot
- `createReturnLots(consumptions, orderId, lotMap)` → restore lots from RTS
- `calcHppFromHargaModal(items, productMap)` → fallback HPP calculation

### Business Rules
- Stock minus **never blocks shipment** — uses last lot's unit cost as fallback
- Bundle components consumed separately with individual lot allocation
- Lot state updates incrementally across items in one order
- Returns preserve original unit costs and expiry dates

### Database Tables
- `inventory_lots` — id, product_id, qty, unit_cost, expires_at, po_id, receive_record_id
- `lot_consumption` — tracks which lots were consumed for which orders

---

## 9. Closing Engine

**File:** `src/lib/closing-engine.ts`

### Process
1. Sum income accounts (40-xxx, 41-xxx) → Credit balance
2. Sum expense accounts (50-xxx through 90-xxx) → Debit balance
3. Generate 3 closing journals:
   - Entry 1: Close income → Ikhtisar Laba Rugi
   - Entry 2: Close expenses → Ikhtisar Laba Rugi
   - Entry 3: Transfer net → Laba Tahun Berjalan (30-500)

### Required Accounts
- 30-400 Ikhtisar Laba Rugi (temporary)
- 30-500 Laba Tahun Berjalan (permanent)
- Kontra accounts use reversed debit/credit logic

---

## 10. XLSX Parsers

### TikTok Parser (`tiktok-parser.ts`)
- 63-column format, 1 row = 1 line item (multiple rows per order)
- Revenue: `SKU Subtotal - Seller Discount`
- TikTok exports non-standard XML format → custom fallback parser
- Group by order ID → combine items

### Shopee Parser (`shopee-parser.ts`)
- 49-column format, 1 row = 1 line item
- Revenue: `Total Pembeli + Diskon Shopee - Voucher - Paket Diskon`
- Standard XLSX via ExcelJS

### Mengantar Parser (`mengantar-parser.ts`)
- HTML-disguised .xls files → DOMParser fallback
- 1 row = 1 order with embedded product list
- Revenue: `productValue` (non-COD) or `productValue - estimatedPrice` (COD)

### Mengantar RTS Parser (`mengantar-rts-parser.ts`)
- Parse return records from Mengantar export
- Match by AWB/order number

### Settlement Parser (`settlement-parser.ts`)
- TikTok settlement XLSX
- Extract fee breakdown per order
- Match to existing orders

### Mengantar Settlement Parser (`mengantar-settlement-parser.ts`)
- Mengantar invoice/settlement data
- COD fee + shipping fee extraction

---

## 11. Supabase RPC Functions (16 total)

| # | RPC | Purpose |
|---|---|---|
| 1 | `process_order_shipment` | Kirim pesanan: 5 steps atomic (stock, journal, piutang, status, activities) |
| 2 | `process_rts` | RTS: reverse stock + journal retur |
| 3 | `process_po_receive` | Terima PO: stock in + lots + journal |
| 4 | `process_reconciliation` | Rekonsiliasi: journal + record + status update |
| 5 | `process_order_delete` | Hapus order: reverse + void + delete |
| 6 | `process_journal_create` | Buat 1 jurnal + lines |
| 7 | `process_journal_void` | Void jurnal + audit |
| 8 | `process_stock_transaction` | Insert stock txn + update product qty |
| 9 | `process_bulk_order_create` | Bulk create orders (atomic) |
| 10 | `process_bulk_return_create` | Bulk create returns |
| 11 | `process_reconciliation_batch` | Batch reconciliation journals |
| 12 | `process_po_return` | PO supplier return |
| 13 | `process_period_closing` | Close period (closing journals) |
| 14 | `process_period_reopen` | Reopen period (void + delete) |
| 15 | `process_bulk_hpp_adjustment` | Bulk patch HPP adjustment |
| 16 | `process_rts_inbound_complete` | Complete RTS inbound session |

### Key Pattern
- All RPCs: `SECURITY DEFINER`, `GRANT EXECUTE to authenticated`
- Atomic: all succeed or all rollback (single PL/pgSQL function)
- Duplicate guard: check by nomor_pesanan / source_type+source_id
- Journal ID format: `JE{YYMM}-{seq}`

---

## 12. Edge Functions (31 total)

### TikTok Shop (8)
| Function | Purpose |
|---|---|
| `tiktok-shop-callback` | OAuth callback |
| `tiktok-shop-sync` | Order sync + action routing |
| `tiktok-shop-sync-rts` | RTS/return sync |
| `tiktok-shop-sync-settlement` | Settlement/Finance API sync |
| `tiktok-shop-ship-package` | Arrange shipment |
| `tiktok-shop-print-label` | Generate A6 shipping label PDF |
| `tiktok-shop-inventory` | Inventory sync (preview + push) |
| `tiktok-shop-cron` | Auto-sync orders + RTS |
| `tiktok-shop-shops` | List authorized shops |

### TikTok Ads (3)
| Function | Purpose |
|---|---|
| `tiktok-ads-callback` | OAuth callback |
| `tiktok-ads-sync` | Manual ad sync |
| `tiktok-ads-config` | Config management |

### Facebook (3)
| Function | Purpose |
|---|---|
| `facebook-sync` | Manual ad sync |
| `facebook-config` | Config management |
| `facebook-data-deletion` | GDPR data deletion callback |

### Google Ads (2)
| Function | Purpose |
|---|---|
| `google-ads-sync-direct` | Receive data from Apps Script |
| `google-ads-keyword-sync` | Keyword-level data sync |

### Mengantar (3)
| Function | Purpose |
|---|---|
| `mengantar-sync` | Order + RTS sync |
| `mengantar-settlement` | Read pre-fetched settlement data |
| `mengantar-config` | Config management |

### ScaleV (2)
| Function | Purpose |
|---|---|
| `scalev-webhook` | Receive order webhooks |
| `scalev-config` | Config management |

### Cron (4)
| Function | Schedule | Purpose |
|---|---|---|
| `cron-sync-facebook` | `:00` hourly | Sync FB ad insights (3 days) |
| `cron-sync-tiktok-ads` | `:05` hourly | Sync TT Ads (GMV Max + WebConv, 3 days) |
| `cron-sync-scalev` | `:20` hourly | Auto-import ScaleV confirmed orders |
| `cron-health-check` | `:30` hourly | Health check all integrations |

### Other (4)
| Function | Purpose |
|---|---|
| `auto-sync` | Legacy auto-sync (deprecated) |
| `generate-journal` | Legacy journal generation |
| `sync-queue` | Process sync queue items |
| `sync-soscom-spend` | Aggregate soscom spend data |

### Auth Pattern (ALL Edge Functions)
```
verify_jwt: false (di config.toml)
Auth dicek di dalam code: supabase.auth.getUser(token)
Browser call via invokeFunction() — manual fetch + Authorization header
```

---

## 13. Database Tables

### Integration Tables
| Table | Purpose | Unique Constraint |
|---|---|---|
| `tiktok_shop_integrations` | TT Shop OAuth + shops | (org_id, open_id) |
| `tiktok_ad_integrations` | TT Ads OAuth | (org_id, advertiser_id) |
| `fb_ad_integrations` | FB Ads config | (org_id, ad_account_id) |
| `google_ad_integrations` | Google Ads sync token | (org_id) |
| `mengantar_integrations` | Mengantar API key | (org_id, store_id) |
| `scalev_integrations` | ScaleV webhook config | (org_id) |

### Staging Tables
| Table | Purpose | Unique Constraint |
|---|---|---|
| `tiktok_shop_orders` | TT Shop order staging | (org_id, tiktok_order_id) |
| `tiktok_shop_returns` | TT Shop RTS staging | (org_id, order_id) |
| `tiktok_shop_settlements` | TT Shop settlement staging | (org_id, order_id, statement_id) |
| `mengantar_orders` | Mengantar order staging | (org_id, mengantar_order_id) |
| `mengantar_returns` | Mengantar RTS staging | (org_id, mengantar_order_id) |
| `mengantar_settlements` | Mengantar settlement staging | — |
| `scalev_orders` | ScaleV webhook data | (org_id, scalev_order_id) |
| `sync_queue` | SKU match queue | (org_id, platform, platform_order_id, sync_type) |

### Ad Report Tables
| Table | Purpose | Unique Constraint |
|---|---|---|
| `tiktok_ad_reports` | TT Ads daily metrics | (org_id, advertiser_id, report_level, campaign_id, item_group_id, item_id, date) |
| `fb_ad_reports` | FB Ads daily metrics | (org_id, store_id, date, channel, sku, ad_id) |
| `ad_entries` | Google Ads daily metrics | — |

### Core Business Tables
| Table | Purpose |
|---|---|
| `orders` | Production orders |
| `order_items` | Order line items |
| `return_records` | RTS/return records |
| `reconciliations` | Reconciliation records |
| `journal_entries` | Accounting journals |
| `journal_entry_lines` | Journal debit/credit lines |
| `accounts` | Chart of accounts |
| `products` | Product catalog |
| `inventory_lots` | FIFO lot tracking |
| `lot_consumption` | Lot usage per order |
| `stock_transactions` | Stock movement history |
| `stores` | Store/toko config + feeMapping |
| `organizations` | Multi-tenant org |
| `org_members` | User-org membership + role |
| `purchase_orders` | Purchase orders |
| `po_items` | PO line items |
| `po_receive_records` | PO receive batches |
| `po_payment_records` | PO payments |
| `supplier_returns` | PO returns |
| `integration_sync_logs` | Sync audit trail |

---

## 14. Environment Variables

### Supabase
```
NEXT_PUBLIC_SUPABASE_URL=https://api.lababersih.com
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
```

### TikTok Shop
```
TIKTOK_SHOP_APP_KEY=...
TIKTOK_SHOP_APP_SECRET=...
TIKTOK_SHOP_AUTH_ENDPOINT=...
TIKTOK_SHOP_BASE_URL=...
TIKTOK_SHOP_WEBHOOK_SECRET=...
```

### TikTok Ads
```
TIKTOK_ADS_APP_ID=...
TIKTOK_ADS_SECRET=...
```

### Resend
```
RESEND_API_KEY=...
```

### Sentry
```
NEXT_PUBLIC_SENTRY_DSN=...
SENTRY_AUTH_TOKEN=...
```

### Vercel
```
NEXT_PUBLIC_SITE_URL=https://app.lababersih.com
```

---

## 15. Resend Email

**File:** `src/lib/email.ts`

- From: `"LabaBersih <noreply@lababersih.com>"`
- Single function: `sendInviteEmail({ to, orgName, role, inviteLink })`
- HTML template: table-based, branded green (#00674F) "L" logo
- Fallback text link for email clients yang gak render HTML
- DNS required: SPF, DKIM, DMARC records di domain

---

## 16. Sentry Error Tracking

**File:** `src/lib/sentry.ts`

- `captureApiError(error, context)` — enriched logging with tags
- `sanitizeErrorMessage(err, fallback)` — strip internal details
- `sanitizeForSentry(obj)` — regex redaction: `/token|secret|password|key|auth/i` → [REDACTED]
- Safe error classes (pass to client): TikTokShopApiError, TikTokAdsApiError, FbApiError
- Everything else → generic message to client, full detail to Sentry

---

## 17. Cron Jobs (pg_cron, 4 active)

| Job ID | Edge Function | Schedule | Purpose |
|---|---|---|---|
| 10 | `cron-sync-facebook` | `:00` every hour | FB ad insights |
| 11 | `cron-sync-tiktok-ads` | `:05` every hour | TT Ads (GMV Max + WebConv) |
| 12 | `cron-sync-scalev` | `:20` every hour | Auto-import ScaleV orders |
| 13 | `cron-health-check` | `:30` every hour | Health check |

All crons use `SUPABASE_SERVICE_ROLE_KEY` for auth.

---

## 18. Critical Business Rules

### Platform Revenue Formulas
| Platform | Formula | JANGAN |
|---|---|---|
| TikTok | `totalHarga = payment.sub_total` | JANGAN kurangi seller_discount (sudah included) |
| Shopee | `totalHarga = Total Pembeli + Diskon Shopee - Voucher - Paket Diskon` | — |
| Mengantar COD | `totalHarga = productValue - estimatedPrice` | JANGAN pakai productValue langsung |
| Mengantar Non-COD | `totalHarga = productValue` | — |
| ScaleV | `totalHarga = product_price` | JANGAN pakai gross_revenue |

### Proportional Distribution
Semua platform pakai algorithm yang SAMA:
1. Hitung `totalRawValue = sum(item.hargaSatuan × qty)`
2. Per item (except last): `proportion = (hargaSatuan × qty) / totalRawValue` → `hargaSatuan = round(totalHarga × proportion / qty)`
3. Last item: `hargaSatuan = round((totalHarga - distributed) / qty)` (capture remainder)

### Status "Lebih Maju Menang"
- Boleh MAJU (dibuat → dikemas → dikirim → selesai)
- DILARANG MUNDUR (dikirim → dikemas)
- Parallel API + XLSX import aman karena rule ini

### Fee Transparency
- SEMUA fee ≠ 0 WAJIB ditampilkan
- Fee dikenal → auto-assign akun
- Fee gak dikenal → tampilkan nama asli, user pilih akun manual

### Settlement H+1 Rule (TikTok)
- API `statement_time` = H (kapan TikTok generate)
- XLSX "settled time" = H+1 (kapan uang masuk)
- Query API: shift date -1 hari

---

## 19. Key Gotchas & Lessons Learned

### Fatal Mistakes (Never Repeat)
1. **TikTok sub_total double subtraction** — sub_total already includes seller_discount
2. **ScaleV gross_revenue vs product_price** — gross_revenue includes shipping, product_price is pure revenue
3. **RTS date filter** — use `lastStatusChange` (kapan jadi RTS), BUKAN order creation date
4. **API tanpa debug** — 4x push gagal karena asumsi API. WAJIB debug route dulu.
5. **TikTok ship 3-second delay** — tracking_number belum ready tanpa delay
6. **HMAC signature params** — MUST sort alphabetically, MUST exclude sign/access_token
7. **Token refresh di cron** — deactivate integration kalau gagal (jangan loop retry)

### Patterns That Work
1. **Staging table pattern** — API → staging → preview → production (never auto-insert)
2. **Proportional distribution** — prevent rounding errors across all platforms
3. **Fire-and-forget logging** — non-fatal sync logs (gak block response)
4. **Batch with fallback** — upsert in chunks of 50, on batch failure retry one-by-one
5. **WIB timezone helpers** — centralized `toWibDateString()`, `todayWibISO()`, `daysAgoWibISO()`
6. **SKU matching cascade** — exact SKU → exact name → SKU in name → name contains
7. **verify_jwt: false + auth in code** — Supabase recommended pattern for ES256 JWT

### Architecture Decision Record
- **v1 final state:** Next.js 0 API routes (frontend only) + 31 Edge Functions (backend) + 16 RPC (DB logic)
- **Service layer:** Browser → `invokeFunction()` → Edge Function → Supabase RPC
- **Direct DB:** Browser → Supabase client (RLS) for simple CRUD
- **Custom domain:** `api.lababersih.com` = Supabase, `app.lababersih.com` = Vercel

---

## File Reference (Key Source Files)

### Integration Libraries (`src/lib/integrations/`)
| File | Platform |
|---|---|
| `tiktok-shop.ts` | TikTok Shop API client (Node.js) |
| `tiktok-shop-mapper.ts` | TikTok order → OrderInput |
| `tiktok-ads.ts` | TikTok Ads API client |
| `facebook.ts` | Facebook Marketing API client |
| `google-ads.ts` | Google Ads helpers |
| `scalev-order-mapper.ts` | ScaleV order → OrderInput |

### Business Engines (`src/lib/`)
| File | Purpose |
|---|---|
| `auto-journal-engine.ts` | 13 journal types + fee calculation |
| `fifo-engine.ts` | FIFO/FEFO inventory costing |
| `closing-engine.ts` | Period closing |
| `accounting-constants.ts` | Chart of accounts + ACCT codes |
| `marketing-engine.ts` | Marketing metrics |

### Parsers (`src/lib/`)
| File | Purpose |
|---|---|
| `tiktok-parser.ts` | TikTok XLSX import |
| `shopee-parser.ts` | Shopee XLSX import |
| `mengantar-parser.ts` | Mengantar XLSX import |
| `mengantar-rts-parser.ts` | Mengantar RTS import |
| `settlement-parser.ts` | TikTok settlement XLSX |
| `mengantar-settlement-parser.ts` | Mengantar settlement |

### Shared (Deno — Edge Functions)
| File | Purpose |
|---|---|
| `_shared/tiktok-shop.ts` | TikTok Shop API client (Deno) |

### Migrations
- **123 migration files** in `supabase/migrations/`
- RPC functions: migrations 092–117
- Core tables: migrations 001–091
