On this page
This runbook is for operators receiving raw customer data (CSV or Excel) and pushing it through the YNG platform. It covers the supported data shapes, the column-mapping wizard, the most common parse failures, and the fastest path to a green import.
TL;DR#
cd apps/web→ open/connectors/uploadswhile signed into the target org.- Drop in a
.csv,.xlsx, or.xlsfile under 8 MiB. - Pick the upload type (orders / creators / inventory / creator_costs / targets / products) for Wave 1‑2 streams, or a mapping template for Wave 3 streams (ad spend, payouts, returns, warehouse moves, etc.).
- Submit. The worker parses, normalises, and writes to staging (
import_staging_rows) or bronze (raw_events), then promotes to silver. - Watch the job row in
manual_upload_jobsforstatusandrejected_reasons.
Supported data types#
| Stream | Wave | Path | Backing table |
|---|---|---|---|
orders |
1 | hardcoded aliases | raw_events → orders |
creators |
1 | hardcoded aliases | raw_events → creators |
inventory |
1 | hardcoded aliases | raw_events → inventory_snapshots |
creator_costs |
2 | hardcoded aliases | raw_events → creator_costs |
targets |
2 | hardcoded aliases | raw_events → targets |
products |
2 | hardcoded aliases | raw_events → products |
ad_spend |
3 | configurable template | import_staging_rows → ad_spend_daily |
budget_plan |
3 | configurable template | import_staging_rows → budget_plans |
payouts / returns / shipments / warehouse_movements / promotions |
3 | configurable template | import_staging_rows → per-stream silver |
For Wave 1‑2 data types the column aliases live in packages/connectors/core/src/manual-upload.ts (Vietnamese + English headers). For Wave 3, create a mapping template under /connectors/uploads/templates describing the column transforms.
Column mapping template anatomy#
A template is a Zod-validated JSON document with three required sections:
{
"formatVersion": 1,
"targetStream": "ad_spend", // which silver table this lands in
"parser": {
"delimiter": ",", // , ; | \t
"quote": "\"",
"skipLines": 0, // skip title rows / metadata
"encoding": "utf8", // utf8 | utf16le | latin1 | win1258
"trimHeaders": true,
"headerCaseInsensitive": true,
},
"columns": [
{
"source": "Date", // CSV header
"target": "day", // canonical field
"required": true,
"transform": {
"kind": "date",
"format": "DD/MM/YYYY", // Vietnamese day-first dates
"output": "date", // 'iso' for full ISO-8601 timestamps
},
},
{
"source": "Spend",
"target": "spend_vnd",
"required": true,
"transform": {
"kind": "int",
"thousandsSep": ",",
"decimalSep": ".",
"stripCurrency": true, // strips đ, VND, $, %, whitespace
"parenthesesAsNegative": false, // accounting exports
},
},
],
"required": ["platform", "day", "spend_vnd"],
"dedupeKey": ["platform", "campaign_external_id", "day"],
}
Transform kinds (closed set)#
Adding a new kind is a code change in packages/connectors/core/src/mapping/transforms.ts. The set today:
- pass — keep the raw string.
- string — trim, case fold, collapse whitespace, default fallback.
- int — strip thousands separator, strip currency symbols (
stripCurrency: trueby default), drop decimal portion, optionalmultiplyBy, optionalparenthesesAsNegativefor accounting exports. - decimal — same as int but emits a float; never use for VND (CLAUDE.md §3 — VND is integer).
- date — format-driven parser (
DD/MM/YYYY,YYYY-MM-DD,M/D/YYYY, plus time variants). Outputdate→YYYY-MM-DD, outputiso→ full ISO-8601 in UTC. - enum — explicit source-value → canonical-value map. Unknown values default, reject, or pass.
- concat — join multiple source columns with a separator. Used for synthetic dedupe keys.
- constant — emit a fixed value regardless of source.
Things that bite (and the fix)#
| Symptom | Likely cause | Fix |
|---|---|---|
cannot parse int from "1,234,000đ" |
Currency symbol in VND column | stripCurrency: true (now the default for int/decimal) |
cannot parse int from "1.234.000" |
European thousands separator | Set thousandsSep: "." and decimalSep: "," in the template |
cannot parse date "15/05/2026" with format "MM/DD/YYYY" |
Day-first dates parsed as US format | Set format: "DD/MM/YYYY" in the date transform |
cannot parse date "30/02/2026" |
Impossible calendar date (Feb 30) | Real bug in source — surface to customer; the parser correctly rejects |
Vietnamese diacritics appear as Bún bɄ |
File encoded as Windows-1258 (legacy VN) | Set encoding: "win1258" in the template's parser block |
unsupported_extension on .xlsx upload |
xlsx peer dep not installed on worker |
Verify pnpm --filter @yng/worker list xlsx shows a version; reinstall if absent |
file_too_large (8 MiB cap) |
Bulk historical backfill | Split into multiple smaller files OR ship via S3/Minio when the object-store path lands |
| Duplicate rows ingested twice on replay | Dedupe key collapsed to [object Object] (old bug) |
Fixed: dedupe-key builder unwraps __pendingDate markers correctly |
| Negative numbers ignored or 0 | Source uses parens (1234) for negatives |
Set parenthesesAsNegative: true on the int/decimal transform |
Tomorrow's customer file — recommended pre-flight#
- Open the file in a text editor (not Excel — Excel silently re-encodes). Confirm encoding via
file <path>. Vietnamese exports from older POS systems can be Windows-1258. - Inspect the header row. If it's Vietnamese (
Mã đơn,Ngày tạo) AND the customer uses the Wave 1-2ordersstream, the hardcoded aliases handle it — no template needed. - Spot-check three rows for: currency symbols, date format, and any non-printable characters.
- Upload the first 100 rows as a test. Validate
manual_upload_jobs.rejected_reasonsis empty. - Upload the full file only after the test passes.
Verifying a successful upload#
-- Recent jobs and their counts
SELECT id, upload_type, status, rows_processed, rows_upserted, rows_rejected,
LEFT(COALESCE(rejected_reasons::text, ''), 200) AS sample_errors
FROM manual_upload_jobs
WHERE org_id = '<org-uuid>'
ORDER BY created_at DESC
LIMIT 5;
For Wave 3 templated imports:
SELECT status, COUNT(*) FROM import_staging_rows
WHERE template_id = '<template-uuid>'
GROUP BY status;
queued → still being promoted. promoted → made it to the silver table. rejected → check error column on the row.
When to escalate#
- The same row hash keeps producing different dedupe keys → file an issue against
@yng/connectors-core/mapping/applywith the row contents. - A non-trivial fraction of rows are
rejectedand the error mentions Zod issue paths → the canonical schema inpackages/connectors/core/src/mapping/streams.tsrejected the post-transform shape. Surface to engineering. - A
.xlsxfile fails withunsupported_extensionafter the worker has been redeployed → confirmxlsxis inapps/worker/package.jsondeps (not peer).
Test scripts#
End-to-end smoke tests for the upload pipeline live in:
apps/web/src/__tests__/import-mapping.test.ts— pure transform unit tests (Vietnamese dates, currency stripping, parens-as-negative, dedupe-key stability, CSV → accept/reject decisions, idempotent replay).apps/web/e2e/manual-upload.spec.ts— Playwright smoke ensuring/connectors/uploadsand the templates editor are reachable while authenticated.
Run them with:
pnpm --filter @yng/web test # vitest unit suite
pnpm --filter @yng/web test:e2e # playwright (workers=1)