Purchasing + Sales Refactor Plan
Status: Approved — all open questions answered (see §9).
Status (2026-06-23): shipped, then superseded. Migrations 038–041 live under
merchant_database/db_kesles_merchant/migrations/legacy/. The purchasing/sales source-of-truth has since moved out ofdb_kesles_merchanttoinventory_service(db_kesles_merchant_inventory) andorder_service(db_kesles_merchant_order); themerchant.*purchasing/sales tables were dropped (migrations v1/089, v1/093, v1/094). This plan is retained as the original design record. Owner: Dashboard team. Related tables:merchant.registration_device_offers,merchant.payment_device_models,merchant.payment_terminal_inventory,merchant.merchants,merchant.merchant_registration_requests. Note (2026-05-26):payment_device_modelsdanpayment_terminal_inventoryakan di-rename/restruktur ke tabel generikproducts+product_inventory_itemsviaproduct-catalog-restructure-plan.md. Kolomdevice_model_iddipurchase_order_items,goods_receipt_items,sales_quotations,sales_order_itemsakan di-rename keproduct_iddi Phase 5 plan tersebut. Terminology plan ini (device_model_id) tetap valid hingga Phase 5 selesai.
1. Context & Problem
The current Device Offer form (merchant.registration_device_offers) mixes two concerns in one row:
- Procurement cost (HPP) —
purchase_price_amount,customs_fee_amount,vat_amount,inbound_shipping_cost_amount,total_capital_amount,margin_percent,sales_vat_amount. - Selling catalog —
device_price_amount,shipping_fee_amount,promo_amount,stock_quantity.
Effects:
- Operators get confused separating "what we pay the vendor" vs "what we sell to the merchant".
- There is no traceability to the vendor — Kesles doesn't record who the supplier is, the PO number, or when goods were received.
- There is no Purchase Order → Goods Receipt → Invoice → Payment workflow — every procurement step is implicit.
- The Sales side (Order, Shipping) is still a frontend mock (
apps/merchant_dashboard/lib/dashboard/features/home/presentation/state/order_shipping_mock_store.dart). History is lost on refresh.
The refactor introduces a Group Purchasing module (Vendors, Purchase Order, Goods Receipt, Purchase Invoice, Vendor Payment, Purchasing Report) and tidies up the Group Sales module (Quotation, Sales Order, Shipping Order, Customer, Inventory).
Critical constraint: the snapshot pattern in merchants.device_offer_code / device_price_amount / shipping_fee_amount / promo_amount and merchant_registration_requests.* (migration 025) MUST NOT be touched — per-merchant price history must stay intact.
2. Terminology Mapping (user → DB)
| User term | DB artefact | Notes |
|---|---|---|
| Quotation | merchant.sales_quotations (rename of registration_device_offers) | Selling-only; HPP fields removed |
| Sales Order | merchant.sales_orders + sales_order_items (new) | Replace OrderMockRecord |
| Shipping Order | merchant.shipping_orders + shipping_order_units (new) | Replace ShippingMockRecord |
| Customer | merchant.merchants | Label only, schema unchanged |
| Inventory | merchant.payment_terminal_inventory | Label only, schema unchanged |
| Vendor | merchant.vendors (new) | |
| Purchase Order | merchant.purchase_orders + purchase_order_items (new) | |
| Goods Receipt | merchant.goods_receipts + goods_receipt_items (new) | On post → create rows in payment_terminal_inventory |
| Purchase Invoice / Bills | merchant.purchase_invoices + purchase_invoice_items (new) | 3-way match against PO + GR |
| Vendor Payment | merchant.vendor_payments + vendor_payment_applications (new) | AP ledger |
| Purchasing Report | computed (handler + view), no dedicated table | |
| Cost history for margin | merchant.device_purchase_cost_history (new) | Append-only, seeded once from old HPP |
3. Database Changes (Migrations 038–041)
Run order: 038 → 039 → 040 → 041.
Convention: every amount is bigint (IDR), soft delete via deleted_at, timestamp timestamptz not null default now().
3.1 Migration 038 — Purchasing + Sales tables
New tables in the merchant schema:
- vendors — supplier master. Columns:
vendor_code(UNIQUE ci),legal_name,display_name,npwp,contact_person,email,phone,address_line,city,province,postal_code,country,payment_term_days,bank_name,bank_account_number,bank_account_holder,is_active,notes,metadata jsonb, timestamps. - purchase_orders — PO header.
po_number(UNIQUE, sequentialPO-YYYY-NNNN),vendor_id fk,order_date,expected_delivery_date,incoterm,delivery_address, amount fields,statusCHECKin ('draft','approved','sent','partial_received','received','closed','cancelled'), audit timestamps + user FKs. - purchase_order_items — PO line.
purchase_order_id fk on delete cascade,line_no,device_model_id fk nullable,item_name,quantity_ordered,unit_price_amount,line_discount_amount,line_tax_amount,line_total_amount,quantity_received (default 0),quantity_invoiced (default 0). UNIQUE(purchase_order_id, line_no). CHECKquantity_received BETWEEN 0 AND quantity_ordered. - goods_receipts — GR header.
gr_numberUNIQUE,purchase_order_id fk,vendor_id(denormalised),receipt_date,warehouse_location,statusCHECKin ('draft','posted','cancelled'),received_by_user_id,posted_at. - goods_receipt_items — GR line.
goods_receipt_id fk on delete cascade,purchase_order_item_id fk,device_model_id fk,quantity_received,unit_cost_amount(copy from PO),landed_cost_allocated_amount,serial_numbers text[]. Validation:array_length(serial_numbers) = quantity_receivedon GR post (whendevice_model_id is not null). - purchase_invoices — vendor invoice header.
invoice_number(vendor's),vendor_id fk,purchase_order_id fk nullable,invoice_date,due_date, amount fields,amount_paid (default 0),amount_outstanding generated stored,statusCHECKin ('draft','awaiting_approval','approved','partially_paid','paid','void'),file_url(MinIO),metadata jsonb. UNIQUE(vendor_id, invoice_number)wheredeleted_at is null. - purchase_invoice_items — invoice line. Linked to
goods_receipt_item_id(for 3-way match) +purchase_order_item_id. - vendor_payments — vendor payment.
payment_number(sequentialPAY-YYYY-NNNN),vendor_id fk,payment_date,payment_method,bank_reference,amount,statusCHECKin ('pending','posted','failed','void'),proof_file_url. - vendor_payment_applications — M:N payment vs invoice.
vendor_payment_id fk,purchase_invoice_id fk,amount_applied. The/posthandler updatespurchase_invoices.amount_paid.
The Sales-side tables also live in this migration:
- sales_orders —
order_number(SO-YYYY-NNNN),merchant_id fk,statusCHECKin ('pending','accepted','payment_received','processing','completed','cancelled'),total_amount,payment_proof_url,cancellation_reason. - sales_order_items —
sales_order_id fk on delete cascade,line_no,sales_quotation_id fk,device_model_id fk,quantity,unit_price_amount(snapshot),shipping_fee_amount(snapshot),promo_amount(snapshot),line_total_amount. - shipping_orders —
shipping_number(SHP-YYYY-NNNN),sales_order_id fk,statusCHECKin ('not_processed','packed','in_transit','delivered','returned'),courier,tracking_number. - shipping_order_units —
shipping_order_id fk,payment_terminal_inventory_id fkUNIQUE. On delivery → update the linked unit's statusallocated→delivered+ stampmerchant_id.
All tables get partial indexes where deleted_at is null + status/FK+date indexes following the existing pattern.
3.2 Migration 039 — device_purchase_cost_history
Append-only fact for WAC (weighted-average cost) per device_model_id:
create table merchant.device_purchase_cost_history (
id uuid primary key default gen_random_uuid(),
device_model_id uuid not null references merchant.payment_device_models(id),
goods_receipt_id uuid references merchant.goods_receipts(id),
goods_receipt_item_id uuid references merchant.goods_receipt_items(id),
unit_cost_amount bigint not null,
landed_cost_amount bigint not null default 0,
total_unit_cost_amount bigint generated always as (unit_cost_amount + landed_cost_amount) stored,
quantity int not null default 1,
cost_date date not null default current_date,
currency char(3) not null default 'IDR',
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create index idx_dpch_model_date
on merchant.device_purchase_cost_history (device_model_id, cost_date desc);
View: merchant.v_sales_quotations_with_cost — LEFT JOIN every (future) sales_quotations row to the latest cost history, expose current_capital_amount + current_margin_percent. The Quotation panel reads this view, so the margin is always live without writing back to the quotation row.
3.3 Migration 040 — Seed cost history from device_offers
One row per current registration_device_offers:
unit_cost_amount = purchase_price_amountlanded_cost_amount = customs_fee_amount + vat_amount + inbound_shipping_cost_amountquantity = greatest(stock_quantity, 1)cost_date = current_dategoods_receipt_id = null
Preserves HPP history → the margin view is not NULL after 041 drops the columns.
3.4 Migration 041 — Split → sales_quotations
alter table merchant.registration_device_offers rename to sales_quotations;
-- rename indexes: registration_device_offers_* → sales_quotations_*
alter table merchant.sales_quotations
drop column purchase_price_amount,
drop column customs_fee_amount,
drop column vat_amount,
drop column inbound_shipping_cost_amount,
drop column total_capital_amount,
drop column margin_percent,
drop column sales_vat_amount;
alter table merchant.sales_quotations
add column effective_from date default current_date,
add column effective_to date; -- nullable = currently active
offer_code is NOT renamed → merchants.device_offer_code and merchant_registration_requests.device_offer_code are snapshot columns pointing here. Renaming would force a multi-table rewrite with no user-facing benefit.
3.5 GR-post transactional side-effects
The handler POST /api/dashboard/purchasing/goods-receipts/{id}/post opens a single tx:
- Lock the GR row; validate status =
draft. - Per device line: validate
array_length(serial_numbers) = quantity_received, reject duplicates (PG 23505 → 409). - Insert one
payment_terminal_inventoryrow per serial withmerchant_id=null,status='stock',received_at=now(), auto-generatedterminal_code(reusegenerateTerminalCode()). - Compute landed-cost allocation:
shipping_cost + customs + vat + other_costfrom the PO header, pro-rata perline_total_amount. Save togoods_receipt_items.landed_cost_allocated_amount. - Insert one row per GR line into
device_purchase_cost_history(unit_cost_amountfrom PO,landed_cost_amount = allocated / quantity). - Bump
purchase_order_items.quantity_received. Flip the PO header: all lines received →received; partial →partial_received. - Append
payment_terminal_status_logsper unit (source_type='system',event_title='Received via GR {gr_number}'). - Commit. Flip GR status →
posted, stampposted_at.
Any failure → full rollback (no partial units).
4. Backend (Go)
4.1 File layout (one file per domain, existing pattern)
In services/dashboard_api/internal/app/:
dashboard_vendors.godashboard_purchase_orders.godashboard_goods_receipts.go⚡ transactional GR-postdashboard_purchase_invoices.godashboard_vendor_payments.godashboard_purchasing_reports.godashboard_sales_orders.godashboard_shipping_orders.go- Rename
dashboard_device_offers.go→dashboard_sales_quotations.go(the legacy/master-data/device-offersroute alias stays for 1 release)
New shared helper: rbac.go with requireRoles(profile, roles...) — replace the per-handler profile.hasAnyRole(...) slice pattern.
4.2 Route map
Purchasing:
GET/POST /api/dashboard/purchasing/vendors
GET/PATCH/DELETE /api/dashboard/purchasing/vendors/{id}
GET/POST /api/dashboard/purchasing/purchase-orders
GET/PATCH /api/dashboard/purchasing/purchase-orders/{id} (PATCH only when status='draft')
POST /api/dashboard/purchasing/purchase-orders/{id}/approve (draft→approved)
POST /api/dashboard/purchasing/purchase-orders/{id}/send (approved→sent)
POST /api/dashboard/purchasing/purchase-orders/{id}/cancel
POST /api/dashboard/purchasing/purchase-orders/{id}/close
GET/POST /api/dashboard/purchasing/goods-receipts
GET/PATCH /api/dashboard/purchasing/goods-receipts/{id}
POST /api/dashboard/purchasing/goods-receipts/{id}/post ⚡ tx
POST /api/dashboard/purchasing/goods-receipts/{id}/cancel
GET/POST /api/dashboard/purchasing/purchase-invoices
GET/PATCH /api/dashboard/purchasing/purchase-invoices/{id}
POST /api/dashboard/purchasing/purchase-invoices/{id}/submit
POST /api/dashboard/purchasing/purchase-invoices/{id}/approve (3-way match)
POST /api/dashboard/purchasing/purchase-invoices/{id}/void
POST /api/dashboard/purchasing/purchase-invoices/{id}/upload-file
GET/POST /api/dashboard/purchasing/vendor-payments
POST /api/dashboard/purchasing/vendor-payments/{id}/post
POST /api/dashboard/purchasing/vendor-payments/{id}/void
GET /api/dashboard/purchasing/reports/spending-by-vendor?from&to
GET /api/dashboard/purchasing/reports/open-pos
GET /api/dashboard/purchasing/reports/gr-variance
GET /api/dashboard/purchasing/reports/invoice-aging
GET /api/dashboard/purchasing/reports/cost-trend?device_model_id
Sales:
GET/POST /api/dashboard/sales/quotations (legacy alias: /master-data/device-offers)
GET/PATCH/DELETE /api/dashboard/sales/quotations/{id}
GET/POST /api/dashboard/sales/orders
GET/PATCH /api/dashboard/sales/orders/{id}
POST /api/dashboard/sales/orders/{id}/{accept|payment-received|processing|complete|cancel}
GET/POST /api/dashboard/sales/shipping-orders
POST /api/dashboard/sales/shipping-orders/{id}/{pack|in-transit|deliver|return}
4.3 3-way Match (on Invoice approve)
For each purchase_invoice_items with a goods_receipt_item_id:
quantity ≤ gr_item.quantity_received − already_invoiced_qtyunit_price ∈ [po_line.unit_price × (1−tolerance), po_line.unit_price × (1+tolerance)](default tolerance0.05, env-configurable viaINVOICE_PRICE_TOLERANCE)
Out-of-tolerance → HTTP 422 with an array {line_no, reason, expected, actual}.
A super_admin may override via ?force=true (audit-logged).
5. Frontend (Flutter / apps/merchant_dashboard)
5.1 Menu enum (restructure)
MerchantDashboardMenu:
overview
sales → submenus: quotation, salesOrder, shippingOrder, customers, inventory
purchasing → submenus: vendors, purchaseOrder, goodsReceipt, purchaseInvoice, vendorPayments, purchasingReport
masterData → submenus: partners, paymentDevices, mobileUser (trimmed)
transactions (unchanged)
reports (unchanged)
usersAccess (unchanged)
settings (unchanged)
The merchants enum is removed — its children redistribute into sales.
5.2 RBAC additions (in dashboard_rbac.dart)
New permissions: purchaseRead, purchaseCreate, purchaseApprove, purchaseReceive, purchasePay, salesRead, salesCreate, salesUpdate, salesFulfill, quotationRead, quotationManage, vendorRead, vendorManage, inventoryAdjust.
Role matrix highlights:
super_admin,admin→ everythingfinance→ full Purchasing (approve + pay) + Sales read/update + Quotation manage + Vendor manage + all Reportsoperations→purchaseReceive+inventoryAdjust+ Sales fulfill + read everything Sales/Purchasingpartner_admin→ Sales read (scoped to own merchants) + Quotation read + Reports readsupport,viewer→ read-only
5.3 New panels / dialogs
In apps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/panels/:
vendors_panel.dart+forms/vendor_form_dialog.dartpurchase_orders_panel.dart+forms/purchase_order_form_dialog.dart+purchase_order_detail_dialog.dart(state transition buttons)goods_receipts_panel.dart+forms/goods_receipt_form_dialog.dart+goods_receipt_post_dialog.dart(serial-number grid)purchase_invoices_panel.dart+forms/purchase_invoice_form_dialog.dartvendor_payments_panel.dart+forms/vendor_payment_form_dialog.dartpurchasing_report_panel.dart(reusewidgets/charts/dashboard_charts.dart)
Rename + simplify:
device_offers_panel.dart→sales_quotations_panel.dart(drop the HPP section; only Identity + Selling + Availability + Effective From/To remain)forms/device_offer_form_dialog.dart→forms/sales_quotation_form_dialog.dartorder_panel.dart→sales_order_panel.dart(real API after Ship-7)shipping_panel.dart→shipping_order_panel.dart(real API after Ship-7)
Keep-but-relabel:
merchants_panel.dart— displayed as "Customers" via the menu labelterminal_stock_panel.dart— displayed as "Inventory" via the menu label
The margin display in the Quotation form → a read-only strip below that reads from the v_sales_quotations_with_cost view.
i18n: every new label goes into core/i18n/dashboard_language.dart with EN/ID.
6. Mock → Real API Migration (Phase 4)
Files to retire: apps/merchant_dashboard/lib/dashboard/features/home/presentation/state/order_shipping_mock_store.dart
- New datasources:
sales_order_remote_data_source.dart,shipping_order_remote_data_source.dart,sales_quotation_remote_data_source.dart(pattern fromhome_remote_data_source.dart). - New entities:
domain/entities/sales_order.dart,shipping_order.dart,sales_quotation.dart— replacingOrderMockRecord,ShippingMockRecord,kDeviceCatalog. - Repository contract + impl.
- New state stores:
sales_order_store.dart,shipping_order_store.dart,sales_quotation_store.dart(sameChangeNotifiersurface). Usehome_dashboard_async_state.dart. - Rewire
order_panel,order_detail_dialog,shipping_panel,shipping_detail_dialog,dashboard_sections.dartto the new stores. kDeviceCatalogis replaced bySalesQuotationStorewhich prefetches the active quotations on dashboard load → the existing syncunitPricegetter still works in the row list.- Delete
order_shipping_mock_store.dart. Grep verify:kDeviceCatalog,OrderMockRecord,ShippingMockRecord,OrderShippingMockStore— zero references.
7. Rollout Order (7 Ships, lock-step, never combine DB rename + FE release)
- Ship-1 — Migrations 038 + 039 + 040. No UI visible. Smoke-test: merchant registration + transactions + terminal_stock bulk form still work.
- Ship-2 — Read-only Purchasing UI: Vendors CRUD + PO CRUD (draft only) + menu group placeholder. Finance seeds master data.
- Ship-3 — Purchasing write path: PO approve/send/cancel, GR draft + post (inventory creation), Purchase Invoice CRUD + approve + 3-way match. The bulk-stock form is flipped to "super_admin legacy seed only".
- Ship-4 — Flutter Quotation panel update (drop HPP inputs). Backend still accepts but ignores HPP payload. Ships before 041 so the frontend build doesn't break.
- Ship-5 — Migration 041 (drop HPP columns + rename to
sales_quotations). Backend rename handler, the legacy alias stays for 1 release. - Ship-6 — Flutter menu restructure: enum change, Sales group, Quotation under Sales, trimmed Master Data.
- Ship-7 — De-mock: Phase 4 migration. Plus Vendor Payments + Purchasing Reports + cost trend chart.
Risk mitigations
- Merchant snapshot columns (
merchants.device_offer_code,device_price_amount,shipping_fee_amount,promo_amount) — NOT TOUCHED. Add a regression test: register a merchant end-to-end after every migration. - Dual write path to
payment_terminal_inventory: bulk form (legacy) + GR posting (canonical). Document; restrict the bulk form tosuper_adminin Ship-3. - Serial uniqueness:
uk_payment_terminal_inventory_serial_numberalready exists. The GR post handler translates PG 23505 → a readable 409 with the offending serial. - Multi-currency (IDR + USD) — keep
currency+exchange_rateonpurchase_orders,purchase_invoices,vendor_payments.vendors.default_currencysets the default when creating a new PO. Realised/unrealised FX gain is not handled in v1 — the invoice posted-value = the value until settlement.
8. Effort Estimate
| Phase | Person-days |
|---|---|
| Phase 1 — Migrations (038–041) + view | 4–5 |
| Phase 2 — Backend handlers (9 files + RBAC helper + GR-post tx + 3-way match) | 10–14 |
| Phase 3 — Frontend menu restructure + 6 new panels + forms + i18n | 8–12 |
| Phase 4 — Mock → real migration | 3–5 |
| Phase 5 — Rollout (staging verification, 7 lock-step ships, docs) | 2–3 |
| Total | 27–39 person-days (≈ 5.5–8 weeks for 1 full-stack) |
9. Decisions (answered)
| # | Question | Decision | Notes |
|---|---|---|---|
| 1 | HPP strategy | WAC via view (Option A) | merchant.v_sales_quotations_with_cost LEFT JOINs device_purchase_cost_history + a SQL weighted-avg over the last N GRs. Margin is always live. If perf becomes an issue, upgrade to a MATERIALIZED VIEW with a scheduled refresh. Cost history becomes the single source of truth — no duplication in sales_quotations. |
| 2 | Serial number UX | Manual grid + CSV upload | goods_receipt_post_dialog.dart provides 2 modes: (a) row-per-serial grid input for small qty (<20); (b) "Upload CSV" button → parse file (1 serial_number column per row), validate, preview before post. Duplicate detection runs both frontend + backend. |
| 3 | Currency | Multi-currency: IDR (default) + USD | The currency char(3) and exchange_rate numeric(18,6) columns are KEPT on purchase_orders, purchase_invoices, vendor_payments. The vendor record has default_currency. The dashboard reports in IDR (convert via the rate when the PO/Invoice is created). Realised/unrealised FX gain is not handled in v1 — the invoice posted value = the value used until paid. |
| 4 | 3-way match tolerance | 5% default + super_admin ?force=true override | Env var INVOICE_PRICE_TOLERANCE=0.05. The override is always audit-logged (source_type=manual_override, actor + reason in metadata). |
| 5 | Operations GR approval | Operations approve themselves (v1) | No separate Warehouse role. A second-approver can be added later via the GR_REQUIRE_SECOND_APPROVAL feature flag without a migration. |
| 6 | Landed cost allocation | Per-GR only (v1) | Cross-GR allocation is deferred. Per-GR = pro-rata shipping+customs+VAT from the PO header, distributed by line_total_amount. |
| 7 | ERP integration | Dashboard = source of truth (Option A) | No external API integration in v1. The accounting team manually inputs from the dashboard CSV export when needed. Upgrade path: Ship-N+1 add CSV export templates for Accurate/Jurnal, Ship-N+2 API sync if volume grows. |
| 8 | offer_code rename | Keep offer_code (table renamed, column not) | Table registration_device_offers → sales_quotations. The offer_code column STAYS. In the UI it shows as "Quotation Code" (label only). merchants.device_offer_code + merchant_registration_requests.device_offer_code are not touched (snapshot pattern stays safe). |
10. Verification / Smoke Test per Ship
Run on staging after every Ship:
- Merchant registration still works — register a merchant via the mobile app, verify the
merchants.device_offer_code / device_price_amount / shipping_fee_amount / promo_amountsnapshot is populated. Run BEFORE AND AFTER migration 041. - Transaction flow still works — seed a test transaction via core-api, verify the
merchant.transactionsrow + Dashboard Overview chart reflects it. - The existing terminal_stock bulk form still creates inventory (until Ship-3 flips it to legacy-only).
- Purchase flow (Ship-3+): create Vendor → PO draft → approve → send → GR draft with serials → post GR → verify:
payment_terminal_inventorygains 1 row per serial,status='stock'device_purchase_cost_historygains 1 row per GR linepurchase_order_items.quantity_receivedis bumped- PO header status =
partial_received/received
- Invoice flow (Ship-3): create an Invoice linked to the GR → submit → approve → verify the 3-way match passes; create a mismatched invoice → verify the 422 detail.
- Vendor payment (Ship-7): create a payment → apply to an invoice → post → verify
purchase_invoices.amount_paidupdates, statuspaidif fully settled. - Sales Order flow (Ship-7): create an SO (quotation + qty) → accept → payment-received → processing → complete → verify the
shipping_orderis auto-created → pack → in-transit → deliver → verifypayment_terminal_inventory.status='delivered'+merchant_idis stamped. - Reports (Ship-7): open
/purchasing/reports/spending-by-vendor+/cost-trend— verify non-zero rows. - RBAC probe: log in per role (finance, operations, partner_admin, support, viewer) — verify menu visibility + 403 on forbidden endpoints.
Automated tests:
- Go integration test in
services/dashboard_api/...for transactional GR-post rollback on duplicate serial. - Flutter widget test on the Quotation form — verify the HPP section is gone after Ship-4.
- Go unit test for the 3-way match tolerance: exact bound, off-by-one, override flag.
11. Critical Files Reference
Migrations (new)
merchant_database/db_kesles_merchant/migrations/legacy/038_create_purchasing_and_sales_tables.sqlmerchant_database/db_kesles_merchant/migrations/legacy/039_create_device_purchase_cost_history.sqlmerchant_database/db_kesles_merchant/migrations/legacy/040_seed_cost_history_from_device_offers.sqlmerchant_database/db_kesles_merchant/migrations/legacy/041_rename_device_offers_to_sales_quotations.sql
Backend (new + rename)
services/dashboard_api/internal/app/server.go(route registration)services/dashboard_api/internal/app/rbac.go(new helper)services/dashboard_api/internal/app/dashboard_vendors.goservices/dashboard_api/internal/app/dashboard_purchase_orders.goservices/dashboard_api/internal/app/dashboard_goods_receipts.go⚡services/dashboard_api/internal/app/dashboard_purchase_invoices.goservices/dashboard_api/internal/app/dashboard_vendor_payments.goservices/dashboard_api/internal/app/dashboard_purchasing_reports.goservices/dashboard_api/internal/app/dashboard_sales_orders.goservices/dashboard_api/internal/app/dashboard_shipping_orders.goservices/dashboard_api/internal/app/dashboard_device_offers.go(serves the canonical/api/dashboard/sales/quotationsroute; the planned rename todashboard_sales_quotations.gowas not applied)
Frontend (restructure)
apps/merchant_dashboard/lib/dashboard/features/home/presentation/models/dashboard_menu.dartapps/merchant_dashboard/lib/dashboard/features/home/presentation/models/dashboard_rbac.dartapps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/menu/dashboard_sidebar.dartapps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/sections/dashboard_sections.dartapps/merchant_dashboard/lib/dashboard/features/home/presentation/state/home_dashboard_ui_state.dart- (new panels per §5.3)
Retired
apps/merchant_dashboard/lib/dashboard/features/home/presentation/state/order_shipping_mock_store.dart(Phase 4, Ship-7)