From 2dfe4703568a74410ad984fd64ad14bec50c8068 Mon Sep 17 00:00:00 2001 From: JuliaEdom Date: Mon, 6 Jul 2026 03:43:04 +0300 Subject: [PATCH] =?UTF-8?q?fix(g2-s3):=20close=20audit=20minors=20m4/m5/m6?= =?UTF-8?q?/m7/n2=20=E2=80=94=20spec/seed=20number=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - m5 (fix data): satellite seeds' order_date window 21d -> ~122h (about 5.1 baseline days), 10,000 orders ~ 1,965/day per spec §1; §11 documents why §4 seasonality is deliberately not encoded in a 5-baseline-day seed - m6 (fix data): synthetic_seed.sql gs1_gtin now carries the genuine GS1 mod-10 check digit (pinned 160-digit string, asserted against gs1.gtin13_check_digit by a new invariant test); §12#7 extended to state the vault-seed literals are covered - m7 (fix data): amount bands re-centered on §1 avg checks (mp 1.5-2.8k mean 2,150; d2c 2-4.6k mean 3,300; B2B RU 30-74k mean 52k; dxb 60-120k mean 90k; ala 25-65k mean 45k) with multipliers re-chosen so small branch slices equidistribute; postgres_oltp/seed.sql mirrors the same formulas - m4 (fix spec text + tighten tests): §12#4 restated as the order-weighted aggregate B2B avg check (~54.9k in [30k,80k]) with dxb's 90k as the by-design outlier it always was per §1; invariant tests now assert the aggregates and pin spec-fixed defaults (160 SKUs, 30 suppliers 22/5/2/1, sourcing coverage per SKU) - n2 (relax spec): §1/§10 now state every branch is seeded in RUB and no generator/seed converts FX at runtime; AED/KZT/CNY constants documented as doc-level-only conversion basis Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 30 +++++++++ docs/generator-spec.md | 41 +++++++++--- tests/unit/test_dv2_supplier_reference.py | 14 +++++ tests/unit/test_generator_spec_invariants.py | 62 ++++++++++++++++--- .../agentflow/dv2/postgres_oltp/seed.sql | 20 +++--- warehouse/agentflow/dv2/satellite_seed.sql | 14 ++--- .../dv2/satellite_seed_all_branches.sql | 40 ++++++------ warehouse/agentflow/dv2/synthetic_seed.sql | 21 +++++-- 8 files changed, 180 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6137683..e6187cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to AgentFlow are documented in this file. ## [Unreleased] +### Changed — spec/seed number consistency: daily rate, GTIN check digits, band centering, FX honesty (G2 S3, 2026-07-06) + +- **Seed daily rate now matches §1 (audit m5).** `satellite_seed*.sql` order + dates spread over a ~122-hour (≈ 5.1-day) flat window instead of 21 days: + 10,000 orders ≈ 1,965/day — generator-spec §1's baseline rate. §11 now + documents that §4's monthly seasonality is deliberately not encoded in a + 5-baseline-day seed (a 5-day snapshot cannot express a 12-month curve). +- **Vault-seed GTINs are valid GTIN-13 (audit m6).** `synthetic_seed.sql`'s + `gs1_gtin` values now append the genuine GS1 mod-10 check digit via a + pinned 160-digit string, asserted against `reference/gs1.py`'s + `gtin13_check_digit` by a new invariant test — §12 #7 now holds for the + vault seed too, not only the reference catalog. +- **Amount bands re-centered on §1's average checks (audit m7).** The old + equal-width bands ran ~5% (dxb: ~12%) above target; new bands are centered + and multipliers re-chosen so small branch slices equidistribute: + marketplace 1.5k–2.8k (mean ≈ 2,150), D2C 2k–4.6k (≈ 3,300), B2B RU + 30k–74k (≈ 52k), dxb 60k–120k (≈ 90k), ala 25k–65k (≈ 45k). + `postgres_oltp/seed.sql` mirrors the same formulas. +- **§12 #4 no longer contradicts §1; invariant tests tightened (audit m4).** + §12 #4 now claims the order-weighted aggregate B2B avg check (≈ 54.9k ∈ + [30k, 80k]) and names dxb's 90k export-pallet check as the by-design + outlier. Tests now assert the aggregate averages (not only per-branch + proxies) and pin the spec-fixed defaults: 160 SKUs, 30 suppliers + (22 CN + 5 RU + 2 AE + 1 KZ), sourcing coverage for every SKU. +- **§10 FX constants declared documentation-only (audit n2).** §1/§10 now + state that every branch is seeded in ₽ and no generator or seed performs an + FX conversion at runtime; the pinned AED/KZT/CNY constants remain in + `reference/legend.py` solely as the fixed basis for doc/evidence-level + conversions. + ### Removed — X5 Retail Hero loader deleted; at-scale benchmark retired as historical (G2 S2b, 2026-07-05) - **Deleted `warehouse/agentflow/dv2/loaders/x5_retail_hero/`** in full diff --git a/docs/generator-spec.md b/docs/generator-spec.md index f15ea18..4bd2fe0 100644 --- a/docs/generator-spec.md +++ b/docs/generator-spec.md @@ -22,9 +22,10 @@ Consumers, in execution order: ## 1. Master matrix — baseline day -All money **net of VAT, in ₽** (branch-local currency stored per §10; demo FX -constants convert). "Baseline day" = seasonal multiplier 1.0; the seasonal -calendar (§4) modulates it and averages to exactly 1.0 over the year. +All money **net of VAT, in ₽** — every branch is seeded in ₽; the pinned demo +FX constants of §10 are documentation-only. "Baseline day" = seasonal +multiplier 1.0; the seasonal calendar (§4) modulates it and averages to +exactly 1.0 over the year. | Channel | Branch | Orders/day | Avg check, ₽ | Revenue, ₽/day | | ------- | ------ | ---------: | -----------: | -------------: | @@ -257,11 +258,16 @@ legend. Targets: ## 10. Currencies and determinism -- Vault-side branch currencies: RU = RUB, dxb = AED, ala = KZT. **Pinned demo +- **All seeded amounts are ₽, in every branch.** In the legend narrative dxb + invoices in AED and ala in KZT, but the v1 seeds and the vault store only + the ₽ figures of §1 — no generator or seed performs an FX conversion at + runtime, and cross-branch aggregates work directly in ₽. The **pinned demo FX constants** (not live rates; internally consistent with a 90 ₽/USD - world): `AED = 24.50 ₽`, `KZT = 0.175 ₽`, `CNY = 12.40 ₽`. All cross-branch - aggregates in docs/evidence use these constants; write them wherever a - conversion happens. + world): `AED = 24.50 ₽`, `KZT = 0.175 ₽`, `CNY = 12.40 ₽` — kept in + `reference/legend.py` solely as the fixed conversion basis for any + doc/evidence sentence that quotes a non-₽ figure (e.g. FOB in CNY). If a + future revision stores branch-local currencies, these are the constants it + must use. - Generator seed constant stays `20260626`; everything derives deterministically from it. Timestamps keep today's mechanics (relative `NOW()` in serving demo, `load_ts = now64()` in vault seeds). @@ -281,6 +287,13 @@ Target row counts for the rebuilt `synthetic_seed.sql` + satellites | `hub_marking_code` | 160 SKU GTINs + ~12,000 per-unit code sample (≈ one container), statuses issued 25 / in_circulation 60 / withdrawn 15 | per-product only | | `hub_supplier` | 30 | 40 | +Order dates spread uniformly over a ~122-hour (≈ 5.1-day) window ending at +load time — 10,000 orders / 5.1 days ≈ **1,965 orders/day**, exactly §1's +baseline rate. Baseline days carry seasonal multiplier 1.0 by definition, so +§4's monthly curves are deliberately **not** encoded in this seed: a 5-day +snapshot cannot express a 12-month shape; the seasonality belongs to the +long-horizon generator narrative, not the vault seed. + Customer→branch and order→channel assignments follow §1/§7 proportions; the `multiIf(number % 100 < …)` slicing technique stays, only the cut points move. Order `record_source` reflects the channel: `mp__msk` (marketplace @@ -295,11 +308,19 @@ this list is the definition of "цифры взаимно согласованы 1. Annual revenue (Σ channels × 365 × seasonal avg 1.0) ∈ **[3.5, 5.0] B ₽**. 2. Order-count mix: marketplaces 88–90%, B2B 7–9%, D2C 2–4%. 3. Revenue mix: B2B 65–72% of ₽; marketplaces 27–33%. -4. Avg B2B check ∈ [30k, 80k] ₽; avg marketplace check ∈ [1.5k, 3.0k] ₽ — - the AOV distribution is bimodal with no mass between 10k and 25k. +4. Order-weighted avg B2B check (all B2B branches together) ∈ [30k, 80k] ₽ — + §1 puts it at ≈ 54.9k. Per-branch B2B avg checks span 45k (ala) to 90k + (dxb): the RU + EAEU wholesale channels each sit inside [30k, 80k], while + dxb's 90k export-pallet check sits above that band **by design** (§1) and + is not a violation. Avg marketplace check ∈ [1.5k, 3.0k] ₽. The AOV + distribution is bimodal with no channel average between 10k and 25k. 5. Per SKU: FOB < landed < wholesale < marketplace-net < RRC (§5 ladder). 6. Each seasonal curve's 12 multipliers average exactly 1.0. -7. Every GTIN passes `is_valid_gtin13`, prefix ∈ 460–469. +7. Every GTIN passes `is_valid_gtin13`, prefix ∈ 460–469 — both the + reference-catalog GTINs (minted via `gs1.make_gtin13`) and the vault + seed's `gs1_gtin` literals in `synthetic_seed.sql`, whose check digits are + precomputed with the same GS1 mod-10 algorithm and pinned by the invariant + tests. 8. Every `tnved_code` is one of the §3 headings, 10-digit zero-padded form. 9. Dealer accounts × ordering frequency ⇒ 150–200 B2B orders/day. 10. Branch revenue shares sum to 100%; msk ∈ [55%, 65%]. diff --git a/tests/unit/test_dv2_supplier_reference.py b/tests/unit/test_dv2_supplier_reference.py index 54f103b..241fff7 100644 --- a/tests/unit/test_dv2_supplier_reference.py +++ b/tests/unit/test_dv2_supplier_reference.py @@ -88,6 +88,20 @@ def test_tnved_headings_are_real_format(): # --- generator --------------------------------------------------------------- +def test_build_reference_default_counts_match_spec(): + # generator-spec.md pins the defaults: §3 — 160 SKUs; §6 — 30 suppliers + # split 22 CN + 5 RU + 2 AE + 1 KZ. build_reference() must reproduce them + # exactly (a silent tax-id collision drop in _make_suppliers would shrink + # the supplier list — this pin catches that too). + tables = build_reference() + assert len(tables.products) == 160 + assert len(tables.suppliers) == 30 + by_country: dict[str, int] = {} + for supplier in tables.suppliers: + by_country[supplier.tax_country_code] = by_country.get(supplier.tax_country_code, 0) + 1 + assert by_country == {"CN": 22, "RU": 5, "AE": 2, "KZ": 1} + + def test_build_reference_is_deterministic(): a = build_reference(n_suppliers=25, n_products=120, seed=7) b = build_reference(n_suppliers=25, n_products=120, seed=7) diff --git a/tests/unit/test_generator_spec_invariants.py b/tests/unit/test_generator_spec_invariants.py index c1ac7a4..ef795dc 100644 --- a/tests/unit/test_generator_spec_invariants.py +++ b/tests/unit/test_generator_spec_invariants.py @@ -14,7 +14,7 @@ from warehouse.agentflow.dv2.reference import legend from warehouse.agentflow.dv2.reference.generator import build_reference -from warehouse.agentflow.dv2.reference.gs1 import is_valid_gtin13 +from warehouse.agentflow.dv2.reference.gs1 import gtin13_check_digit, is_valid_gtin13 from warehouse.agentflow.dv2.reference.tnved import TNVED_HEADINGS DV2_ROOT = Path(__file__).resolve().parents[2] / "warehouse" / "agentflow" / "dv2" @@ -92,21 +92,34 @@ def test_invariant_3_revenue_mix(): def test_invariant_4_bimodal_avg_checks_no_mass_in_gap(): - # §1's master matrix pins dxb (re-export, "export pallets", thinner - # margin per §5) at a 90k avg check — outside the general [30k, 80k] B2B - # band the same table implies for the domestic + EAEU wholesale channels. - # Read narrowly: the [30k, 80k] band covers RU + ala wholesale; dxb is a - # documented, table-explicit outlier, not a spec violation. + # §12 #4's primary claim is the *order-weighted aggregate*: avg B2B check + # across all B2B branches together ∈ [30k, 80k] ₽ (§1 puts it at ≈54.9k). + # Per-branch checks span 45k (ala) to 90k (dxb export pallets) — the RU + + # EAEU wholesale channels each sit inside the band, dxb's 90k sits above + # it by design (§1) and is not a violation. + b2b_rows = [ + (orders, check) + for channel, _, orders, check in legend.MASTER_MATRIX + if channel in _B2B_CHANNELS + ] + b2b_avg_check = sum(o * c for o, c in b2b_rows) / sum(o for o, _ in b2b_rows) + assert 30_000 <= b2b_avg_check <= 80_000 + + mp_rows = [ + (orders, check) + for channel, _, orders, check in legend.MASTER_MATRIX + if channel in _MARKETPLACE_CHANNELS + ] + mp_avg_check = sum(o * c for o, c in mp_rows) / sum(o for o, _ in mp_rows) + assert 1_500 <= mp_avg_check <= 3_000 + + # per-branch letter of §12 #4: RU + EAEU wholesale inside [30k, 80k]. domestic_b2b_checks = [ check for channel, branch, _, check in legend.MASTER_MATRIX if channel in _B2B_CHANNELS and branch != "dxb" ] - marketplace_checks = [ - check for channel, _, _, check in legend.MASTER_MATRIX if channel in _MARKETPLACE_CHANNELS - ] assert all(30_000 <= c <= 80_000 for c in domestic_b2b_checks) - assert all(1_500 <= c <= 3_000 for c in marketplace_checks) # no channel's avg check falls in the 10k-25k dead zone (holds for all # channels, including dxb) assert all(not (10_000 < check < 25_000) for _, _, _, check in legend.MASTER_MATRIX) @@ -124,6 +137,12 @@ def test_invariant_5_pricing_ladder_bands_are_disjoint_and_ordered(): def test_invariant_5_pricing_ladder_holds_per_sku(): tables = build_reference() + # Pin the default catalog shape so the per-SKU loop cannot pass vacuously: + # §3 fixes 160 SKUs, §6 fixes 30 suppliers and 1-2 sources per SKU. + assert len(tables.products) == 160 + assert len(tables.suppliers) == 30 + sourced_skus = {s.product_bk for s in tables.sourcing} + assert sourced_skus == {p.product_bk for p in tables.products} rrc_by_sku = {p.product_bk: p.rrc_price for p in tables.products} for sourcing in tables.sourcing: rrc = rrc_by_sku[sourcing.product_bk] @@ -146,11 +165,34 @@ def test_invariant_6_seasonal_curves_average_to_one(): def test_invariant_7_every_gtin_valid_and_in_eaeu_range(): tables = build_reference() + assert len(tables.products) == 160 # §3: fixed catalog size — loop is not vacuous for product in tables.products: assert is_valid_gtin13(product.gtin) assert 460 <= int(product.gtin[:3]) <= 469 +def test_invariant_7_seed_sql_gtin_check_digit_string_is_genuine(): + """synthetic_seed.sql mints its 160 vault-seed GTIN stems as + ``concat(460 + k % 10, lpad(200000 + k * 617, 9, '0'))`` and appends the + k-th character of a pinned 160-digit string as the check digit. Recompute + that string with the genuine GS1 mod-10 algorithm and assert the SQL + carries exactly it — so the seed's GTINs satisfy §12 #7 and cannot drift + from ``gs1.gtin13_check_digit`` silently. If the stem formula changes, + the structural asserts below fail first and point here. + """ + seed_sql = (DV2_ROOT / "synthetic_seed.sql").read_text(encoding="utf-8") + # stem formula guards (both the 160-template and the per-unit block) + assert "lpad(toString(200000 + number * 617), 9, '0')" in seed_sql + assert "lpad(toString(200000 + (number % 160) * 617), 9, '0')" in seed_sql + stems = [f"{460 + k % 10}{200000 + k * 617:09d}" for k in range(160)] + check_digits = "".join(str(gtin13_check_digit(stem)) for stem in stems) + assert check_digits in seed_sql + for stem, digit in zip(stems, check_digits, strict=True): + gtin = stem + digit + assert is_valid_gtin13(gtin) + assert 460 <= int(gtin[:3]) <= 469 + + # --- #8 tnved headings ---------------------------------------------------------- diff --git a/warehouse/agentflow/dv2/postgres_oltp/seed.sql b/warehouse/agentflow/dv2/postgres_oltp/seed.sql index 9f5e745..c15f70b 100644 --- a/warehouse/agentflow/dv2/postgres_oltp/seed.sql +++ b/warehouse/agentflow/dv2/postgres_oltp/seed.sql @@ -53,10 +53,10 @@ CREATE TABLE IF NOT EXISTS ops_dxb.orders ( -- ============ Seed: 50 msk customers + 200 msk orders ============ -- Channels / statuses / amounts mirror satellite_seed.sql (generator-spec.md --- §1/§2): msk carries the marketplace-dominant mix (marketplace 1.5k-3k, d2c --- 2k-5k, b2b 30k-80k ₽ net-of-VAT), status ladder pending/confirmed/shipped/ --- delivered/cancelled at 8/10/12/62/8. Amounts stay clear of the 10k-25k --- bimodality dead-zone (§12 #4). +-- §1/§2): msk carries the marketplace-dominant mix (marketplace 1.5k-2.8k, +-- d2c 2k-4.6k, b2b 30k-74k ₽ net-of-VAT — bands centered on §1's avg checks), +-- status ladder pending/confirmed/shipped/delivered/cancelled at 8/10/12/62/8. +-- Amounts stay clear of the 10k-25k bimodality dead-zone (§12 #4). INSERT INTO ops_msk.customers (customer_id, first_name, last_name, email, phone) SELECT 'CUST-MSK-' || lpad(n::text, 4, '0'), @@ -85,9 +85,9 @@ SELECT ELSE 'cancelled' END, CASE - WHEN n <= 186 THEN (1500 + (n * 17) % 1501)::numeric(18, 2) -- marketplace 1.5k-3.0k - WHEN n <= 193 THEN (2000 + (n * 23) % 3001)::numeric(18, 2) -- d2c 2.0k-5.0k - ELSE (30000 + (n * 137) % 50001)::numeric(18, 2) -- b2b msk 30k-80k + WHEN n <= 186 THEN (1500 + (n * 17) % 1301)::numeric(18, 2) -- marketplace 1.5k-2.8k + WHEN n <= 193 THEN (2000 + (n * 37) % 2601)::numeric(18, 2) -- d2c 2.0k-4.6k + ELSE (30000 + (n * 329) % 44001)::numeric(18, 2) -- b2b msk 30k-74k END FROM generate_series(1, 200) AS n ON CONFLICT (order_id) DO NOTHING; @@ -95,8 +95,8 @@ ON CONFLICT (order_id) DO NOTHING; -- ============ Seed: 20 dxb customers + 80 dxb orders ============ -- dxb is the b2b re-export branch (generator-spec.md §1: no marketplace/D2C -- volume). All orders are 'b2b'; amounts follow the export-pallet band --- (60k-130k ₽ net, mirrors satellite_seed_all_branches.sql), well above the --- 10k-25k dead-zone. +-- (60k-120k ₽ net, centered on §1's 90k avg check, mirrors +-- satellite_seed_all_branches.sql), well above the 10k-25k dead-zone. INSERT INTO ops_dxb.customers (customer_id, first_name, last_name, email, phone) SELECT 'CUST-DXB-' || lpad(n::text, 4, '0'), @@ -120,6 +120,6 @@ SELECT WHEN n % 100 < 92 THEN 'delivered' ELSE 'cancelled' END, - (60000 + (n * 191) % 70001)::numeric(18, 2) -- b2b dxb export pallets 60k-130k + (60000 + (n * 355) % 60001)::numeric(18, 2) -- b2b dxb export pallets 60k-120k FROM generate_series(1, 80) AS n ON CONFLICT (order_id) DO NOTHING; diff --git a/warehouse/agentflow/dv2/satellite_seed.sql b/warehouse/agentflow/dv2/satellite_seed.sql index d5f0927..875f451 100644 --- a/warehouse/agentflow/dv2/satellite_seed.sql +++ b/warehouse/agentflow/dv2/satellite_seed.sql @@ -93,7 +93,7 @@ SELECT now64(3) AS load_ts, MD5(concat(order_bk, '|hdr|v1')) AS hash_diff, 'bitrix__msk' AS record_source, - now64(3) - toIntervalHour((number * 7) % (24 * 21)) AS order_date, + now64(3) - toIntervalHour((number * 7) % 122) AS order_date, -- ≈5.1-day flat window: 10,000 orders / 122 h ≈ 1,965/day (§1/§11) channel, multiIf( number % 100 < 8, 'pending', @@ -113,9 +113,9 @@ FROM ( ) AS order_bk, multiIf(number < 8900, 'marketplace', number < 9180, 'd2c', 'b2b') AS channel, multiIf( - number < 8900, toDecimal64(1500 + (number * 17) % 1501, 2), -- marketplace: 1.5k-3.0k - number < 9180, toDecimal64(2000 + (number * 23) % 3001, 2), -- D2C: 2.0k-5.0k - toDecimal64(30000 + (number * 137) % 50001, 2) -- B2B msk: 30k-80k + number < 8900, toDecimal64(1500 + (number * 17) % 1301, 2), -- marketplace: 1.5k-2.8k, mean ≈2,150 (§1) + number < 9180, toDecimal64(2000 + (number * 37) % 2601, 2), -- D2C: 2.0k-4.6k, mean ≈3,300 (§1) + toDecimal64(30000 + (number * 329) % 44001, 2) -- B2B msk: 30k-74k, mean ≈52k (§1) ) AS total_amount FROM numbers(9540) ); @@ -143,9 +143,9 @@ FROM ( '__', lpad(toString(number), 7, '0') ) AS order_bk, multiIf( - number < 8900, toDecimal64(1500 + (number * 17) % 1501, 2), - number < 9180, toDecimal64(2000 + (number * 23) % 3001, 2), - toDecimal64(30000 + (number * 137) % 50001, 2) + number < 8900, toDecimal64(1500 + (number * 17) % 1301, 2), + number < 9180, toDecimal64(2000 + (number * 37) % 2601, 2), + toDecimal64(30000 + (number * 329) % 44001, 2) ) AS subtotal_amount FROM numbers(9540) ); diff --git a/warehouse/agentflow/dv2/satellite_seed_all_branches.sql b/warehouse/agentflow/dv2/satellite_seed_all_branches.sql index 7321637..b5398ed 100644 --- a/warehouse/agentflow/dv2/satellite_seed_all_branches.sql +++ b/warehouse/agentflow/dv2/satellite_seed_all_branches.sql @@ -166,11 +166,11 @@ SELECT now64(3), MD5(concat('bitrix__spb__', lpad(toString(number), 7, '0'), '|hdr|v1')), 'bitrix__spb', - now64(3) - toIntervalHour((number * 7) % (24 * 21)), + now64(3) - toIntervalHour((number * 7) % 122), -- ≈5.1-day flat window (§1/§11, see satellite_seed.sql) 'b2b', multiIf(number % 100 < 8, 'pending', number % 100 < 18, 'confirmed', number % 100 < 30, 'shipped', number % 100 < 92, 'delivered', 'cancelled'), - toDecimal64(30000 + (number * 137) % 50001, 2), + toDecimal64(30000 + (number * 329) % 44001, 2), 0 FROM numbers(9720) WHERE number >= 9540; -- B2B spb [9540,9720) @@ -183,11 +183,11 @@ SELECT now64(3), MD5(concat('bitrix__ekb__', lpad(toString(number), 7, '0'), '|hdr|v1')), 'bitrix__ekb', - now64(3) - toIntervalHour((number * 7) % (24 * 21)), + now64(3) - toIntervalHour((number * 7) % 122), -- ≈5.1-day flat window (§1/§11, see satellite_seed.sql) 'b2b', multiIf(number % 100 < 8, 'pending', number % 100 < 18, 'confirmed', number % 100 < 30, 'shipped', number % 100 < 92, 'delivered', 'cancelled'), - toDecimal64(30000 + (number * 137) % 50001, 2), + toDecimal64(30000 + (number * 329) % 44001, 2), 0 FROM numbers(9850) WHERE number >= 9720; -- B2B ekb [9720,9850) @@ -200,11 +200,11 @@ SELECT now64(3), MD5(concat('bitrix__dxb__', lpad(toString(number), 7, '0'), '|hdr|v1')), 'bitrix__dxb', - now64(3) - toIntervalHour((number * 7) % (24 * 21)), + now64(3) - toIntervalHour((number * 7) % 122), -- ≈5.1-day flat window (§1/§11, see satellite_seed.sql) 'b2b', multiIf(number % 100 < 8, 'pending', number % 100 < 18, 'confirmed', number % 100 < 30, 'shipped', number % 100 < 92, 'delivered', 'cancelled'), - toDecimal64(60000 + (number * 191) % 70001, 2), -- export pallets: thinner margin, bigger tickets (§5) + toDecimal64(60000 + (number * 355) % 60001, 2), -- export pallets 60k-120k, mean ≈90k (§1/§5) 0 FROM numbers(9925) WHERE number >= 9850; -- B2B dxb [9850,9925) @@ -217,11 +217,11 @@ SELECT now64(3), MD5(concat('bitrix__ala__', lpad(toString(number), 7, '0'), '|hdr|v1')), 'bitrix__ala', - now64(3) - toIntervalHour((number * 7) % (24 * 21)), + now64(3) - toIntervalHour((number * 7) % 122), -- ≈5.1-day flat window (§1/§11, see satellite_seed.sql) 'b2b', multiIf(number % 100 < 8, 'pending', number % 100 < 18, 'confirmed', number % 100 < 30, 'shipped', number % 100 < 92, 'delivered', 'cancelled'), - toDecimal64(25000 + (number * 151) % 45001, 2), + toDecimal64(25000 + (number * 263) % 40001, 2), 0 FROM numbers(10000) WHERE number >= 9925; -- B2B ala [9925,10000) @@ -236,9 +236,9 @@ SELECT now64(3), MD5(concat('bitrix__spb__', lpad(toString(number), 7, '0'), '|prc|v1')), '1c__spb', - toDecimal64(30000 + (number * 137) % 50001, 2), - toDecimal64((30000 + (number * 137) % 50001) * 0.02 * (number % 4), 2), - toDecimal64((30000 + (number * 137) % 50001) * 0.20, 2), + toDecimal64(30000 + (number * 329) % 44001, 2), + toDecimal64((30000 + (number * 329) % 44001) * 0.02 * (number % 4), 2), + toDecimal64((30000 + (number * 329) % 44001) * 0.20, 2), toDecimal64(500 + (number % 3) * 300, 2), 0 FROM numbers(9720) @@ -252,9 +252,9 @@ SELECT now64(3), MD5(concat('bitrix__ekb__', lpad(toString(number), 7, '0'), '|prc|v1')), '1c__ekb', - toDecimal64(30000 + (number * 137) % 50001, 2), - toDecimal64((30000 + (number * 137) % 50001) * 0.02 * (number % 4), 2), - toDecimal64((30000 + (number * 137) % 50001) * 0.20, 2), + toDecimal64(30000 + (number * 329) % 44001, 2), + toDecimal64((30000 + (number * 329) % 44001) * 0.02 * (number % 4), 2), + toDecimal64((30000 + (number * 329) % 44001) * 0.20, 2), toDecimal64(500 + (number % 3) * 300, 2), 0 FROM numbers(9850) @@ -268,9 +268,9 @@ SELECT now64(3), MD5(concat('bitrix__dxb__', lpad(toString(number), 7, '0'), '|prc|v1')), '1c__dxb', - toDecimal64(60000 + (number * 191) % 70001, 2), - toDecimal64((60000 + (number * 191) % 70001) * 0.02 * (number % 4), 2), - toDecimal64((60000 + (number * 191) % 70001) * 0.05, 2), -- DXB VAT 5% + toDecimal64(60000 + (number * 355) % 60001, 2), + toDecimal64((60000 + (number * 355) % 60001) * 0.02 * (number % 4), 2), + toDecimal64((60000 + (number * 355) % 60001) * 0.05, 2), -- DXB VAT 5% toDecimal64(500 + (number % 3) * 300, 2), 0 FROM numbers(9925) @@ -284,9 +284,9 @@ SELECT now64(3), MD5(concat('bitrix__ala__', lpad(toString(number), 7, '0'), '|prc|v1')), '1c__ala', - toDecimal64(25000 + (number * 151) % 45001, 2), - toDecimal64((25000 + (number * 151) % 45001) * 0.02 * (number % 4), 2), - toDecimal64((25000 + (number * 151) % 45001) * 0.12, 2), -- KZ VAT 12% + toDecimal64(25000 + (number * 263) % 40001, 2), + toDecimal64((25000 + (number * 263) % 40001) * 0.02 * (number % 4), 2), + toDecimal64((25000 + (number * 263) % 40001) * 0.12, 2), -- KZ VAT 12% toDecimal64(500 + (number % 3) * 300, 2), 0 FROM numbers(10000) diff --git a/warehouse/agentflow/dv2/synthetic_seed.sql b/warehouse/agentflow/dv2/synthetic_seed.sql index ab64ce0..ba3e780 100644 --- a/warehouse/agentflow/dv2/synthetic_seed.sql +++ b/warehouse/agentflow/dv2/synthetic_seed.sql @@ -173,9 +173,12 @@ ARRAY JOIN arrayMap( -- sat_marking_code_gs1__1c__global: status for both the 160 SKU-level -- templates ('issued') and the ~12,000 per-unit sample (25/60/15 split, --- §11). gs1_gtin here is a synthetic, unverified code stem — not the --- reference package's genuine GS1-checked GTIN (which lives in the --- ref__global satellite via reference/build.py); serial_number carries the +-- §11). gs1_gtin identities are synthetic (deterministic stem per SKU slot, +-- distinct from the reference package's ref__global GTINs), but each carries +-- the genuine GS1 mod-10 check digit so is_valid_gtin13 passes (§12 #7): the +-- 13th char comes from the pinned 160-digit string below, precomputed over +-- the 160 stems and asserted against reference/gs1.py's gtin13_check_digit +-- by tests/unit/test_generator_spec_invariants.py. serial_number carries the -- per-unit distinction. INSERT INTO rv.sat_marking_code_gs1__1c__global (marking_code_hk, load_ts, hash_diff, record_source, @@ -186,14 +189,22 @@ SELECT FROM ( SELECT concat('CZ-SKU-', lpad(toString(number), 5, '0')) AS marking_code_bk, - concat(toString(460 + (number % 10)), lpad(toString(200000 + number * 617), 9, '0')) AS gs1_gtin, + concat( + toString(460 + (number % 10)), + lpad(toString(200000 + number * 617), 9, '0'), + substring('6520850863093096493187417410759759859852083086326086386419718742019752952985385309376309618641041041642975975208205308307631631974541874974298298598207537530960', number + 1, 1) + ) AS gs1_gtin, CAST(NULL, 'Nullable(String)') AS serial_number, 'issued' AS marking_status FROM numbers(160) UNION ALL SELECT concat('CZU-', lpad(toString(number % 160), 5, '0'), '-', lpad(toString(intDiv(number, 160)), 7, '0')) AS marking_code_bk, - concat(toString(460 + ((number % 160) % 10)), lpad(toString(200000 + (number % 160) * 617), 9, '0')) AS gs1_gtin, + concat( + toString(460 + ((number % 160) % 10)), + lpad(toString(200000 + (number % 160) * 617), 9, '0'), + substring('6520850863093096493187417410759759859852083086326086386419718742019752952985385309376309618641041041642975975208205308307631631974541874974298298598207537530960', (number % 160) + 1, 1) + ) AS gs1_gtin, lpad(toString(intDiv(number, 160)), 7, '0') AS serial_number, multiIf(number % 100 < 25, 'issued', number % 100 < 85, 'in_circulation', 'withdrawn') AS marking_status FROM numbers(12000)