package parity

import (
	"context"
	"fmt"
	"strings"
	"time"

	qdb "github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/TaruLab/keluargamart-backend/internal/finance"
	"github.com/TaruLab/keluargamart-backend/internal/purchasing"
	"github.com/TaruLab/keluargamart-backend/internal/sales"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"
)

// db is the shared in-memory "database" that the three module stores read and
// write, simulating the single PostgreSQL schema.
type db struct {
	nextID     int64
	customer   map[int64]bool
	supplier   map[int64]bool
	product    map[int64]bool
	prices     map[int64]decimal.Decimal
	custByUser map[int64]int64
	stock      map[string]decimal.Decimal // "pid:wid" -> quantity
	reserved   map[string]decimal.Decimal
	orders     map[int64]*sales.SalesOrder
	orderIt    map[int64][]sales.SOItem
	pos        map[int64]*purchasing.PurchaseOrder
	poIt       map[int64][]purchasing.POItem
	grs        map[int64]*purchasing.GoodsReceipt
	grIt       map[int64][]purchasing.GRItem
	invoices   map[int64]*finance.Invoice
	payments   map[int64]*finance.Payment
	cashflow   map[int64]*finance.Cashflow
	soHistory  []qdb.SalesOrderStatusHistory
	vouchers   map[int64]*purchasing.Voucher
}

func newDB() *db {
	d := &db{
		nextID:     1,
		customer:   map[int64]bool{1: true},
		supplier:   map[int64]bool{1: true},
		product:    map[int64]bool{1: true, 2: true},
		prices:     map[int64]decimal.Decimal{1: decimal.NewFromInt(100), 2: decimal.NewFromInt(50)},
		custByUser: map[int64]int64{},
		stock:      map[string]decimal.Decimal{},
		reserved:   map[string]decimal.Decimal{},
		orders:     map[int64]*sales.SalesOrder{},
		orderIt:    map[int64][]sales.SOItem{},
		pos:        map[int64]*purchasing.PurchaseOrder{},
		poIt:       map[int64][]purchasing.POItem{},
		grs:        map[int64]*purchasing.GoodsReceipt{},
		grIt:       map[int64][]purchasing.GRItem{},
		invoices:   map[int64]*finance.Invoice{},
		payments:   map[int64]*finance.Payment{},
		cashflow:   map[int64]*finance.Cashflow{},
		vouchers:   map[int64]*purchasing.Voucher{},
	}
	return d
}

func (d *db) newID() int64 {
	id := d.nextID
	d.nextID++
	return id
}

func stockKey(productID, warehouseID int64) string {
	return fmt.Sprintf("%d:%d", productID, warehouseID)
}

func (d *db) setStock(productID, warehouseID int64, delta decimal.Decimal) {
	k := stockKey(productID, warehouseID)
	d.stock[k] = d.stock[k].Add(delta)
}

func (d *db) stockQty(productID, warehouseID int64) decimal.Decimal {
	return d.stock[stockKey(productID, warehouseID)]
}

func (d *db) price(productID int64) decimal.Decimal {
	return d.prices[productID]
}

func (d *db) setReserved(productID, warehouseID int64, delta decimal.Decimal) {
	k := stockKey(productID, warehouseID)
	d.reserved[k] = d.reserved[k].Add(delta)
}

func (d *db) reservedQty(productID, warehouseID int64) decimal.Decimal {
	return d.reserved[stockKey(productID, warehouseID)]
}

// --- tx ----------------------------------------------------------------

type fakeTx struct{}

func (fakeTx) QueryRow(context.Context, string, ...any) pgx.Row { return pgxRow{} }
func (fakeTx) Query(context.Context, string, ...any) (pgx.Rows, error) {
	return nil, nil
}
func (fakeTx) Exec(context.Context, string, ...any) (pgconn.CommandTag, error) {
	return pgconn.CommandTag{}, nil
}
func (fakeTx) Commit(context.Context) error   { return nil }
func (fakeTx) Rollback(context.Context) error { return nil }

type pgxRow struct{}

func (pgxRow) Scan(dest ...any) error { return pgx.ErrNoRows }

// --- seq ---------------------------------------------------------------

type fakeSeq struct{ n map[string]int64 }

func newFakeSeq() *fakeSeq { return &fakeSeq{n: map[string]int64{}} }

func (s *fakeSeq) Sequence(_ context.Context, prefix string, _ time.Time, _ any) (int64, error) {
	s.n[prefix]++
	return s.n[prefix], nil
}

// --- sales.Store -------------------------------------------------------

type salesStore struct{ d *db }

func (s *salesStore) Begin(context.Context) (sales.Tx, error) { return fakeTx{}, nil }
func (s *salesStore) CreateOrder(_ context.Context, _ qdb.DBTX, p qdb.CreateSalesOrderParams) (qdb.CreateSalesOrderRow, error) {
	o := &sales.SalesOrder{
		ID:              s.d.newID(),
		SoNumber:        p.SoNumber,
		CustomerID:      p.CustomerID,
		WarehouseID:     int8OrNil(p.WarehouseID),
		SalesID:         int8OrNil(p.SalesID),
		OrderDate:       pgtzDateToStr(p.OrderDate),
		Status:          p.Status,
		OrderType:       p.OrderType,
		Subtotal:        p.Subtotal,
		TaxAmount:       p.TaxAmount,
		DiscountAmount:  p.DiscountAmount,
		TotalAmount:     p.TotalAmount,
		PaidAmount:      p.PaidAmount,
		RemainingAmount: p.RemainingAmount,
		ShippingCost:    p.ShippingCost,
		ShippingAddress: pgtzTextToStrPtr(p.ShippingAddress),
		Notes:           pgtzTextToStrPtr(p.Notes),
		DeliveryDate:    pgtzDateToStrPtr(p.DeliveryDate),
		CreatedAt:       time.Now(),
		UpdatedAt:       time.Now(),
	}
	s.d.orders[o.ID] = o
	return qdb.CreateSalesOrderRow{ID: o.ID, CreatedAt: tsToPg(o.CreatedAt), UpdatedAt: tsToPg(o.UpdatedAt)}, nil
}
func (s *salesStore) UpdateOrder(_ context.Context, _ qdb.DBTX, p qdb.UpdateSalesOrderParams) error {
	o, ok := s.d.orders[p.ID]
	if !ok {
		return sales.ErrNotFound
	}
	o.SoNumber = p.SoNumber
	o.CustomerID = p.CustomerID
	o.WarehouseID = int8OrNil(p.WarehouseID)
	o.SalesID = int8OrNil(p.SalesID)
	o.OrderDate = pgtzDateToStr(p.OrderDate)
	o.Status = p.Status
	o.OrderType = p.OrderType
	o.Subtotal = p.Subtotal
	o.TaxAmount = p.TaxAmount
	o.DiscountAmount = p.DiscountAmount
	o.TotalAmount = p.TotalAmount
	o.PaidAmount = p.PaidAmount
	o.RemainingAmount = p.RemainingAmount
	o.ShippingCost = p.ShippingCost
	o.ShippingAddress = pgtzTextToStrPtr(p.ShippingAddress)
	o.Notes = pgtzTextToStrPtr(p.Notes)
	o.DeliveryDate = pgtzDateToStrPtr(p.DeliveryDate)
	o.UpdatedAt = time.Now()
	return nil
}
func (s *salesStore) DeleteOrder(_ context.Context, _ qdb.DBTX, id int64) error {
	delete(s.d.orders, id)
	return nil
}
func (s *salesStore) OrderByID(_ context.Context, id int64) (qdb.GetSalesOrderByIDRow, error) {
	o, ok := s.d.orders[id]
	if !ok {
		return qdb.GetSalesOrderByIDRow{}, sales.ErrNotFound
	}
	return salesOrderRow(o), nil
}
func (s *salesStore) OrderShiftID(context.Context, qdb.DBTX, int64) (pgtype.Int8, error) {
	return pgtype.Int8{}, nil
}
func (s *salesStore) OrderByIDForUpdate(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetSalesOrderByIDForUpdateRow, error) {
	row, err := s.OrderByID(context.Background(), id)
	return qdb.GetSalesOrderByIDForUpdateRow(row), err
}
func (s *salesStore) SupervisorsByOrders(context.Context, []int64) (map[int64]sales.OrderSupervisor, error) {
	return map[int64]sales.OrderSupervisor{}, nil
}
func (s *salesStore) ListOrders(context.Context, sales.SOFilter) ([]qdb.ListSalesOrdersRow, int, error) {
	out := make([]qdb.ListSalesOrdersRow, 0, len(s.d.orders))
	for _, o := range s.d.orders {
		out = append(out, salesListOrderRow(o))
	}
	return out, len(out), nil
}
func (s *salesStore) SalesOrdersSummary(_ context.Context, _ []int64) (sales.SalesOrdersSummary, error) {
	return sales.SalesOrdersSummary{}, nil
}
func (s *salesStore) MyOrdersSummary(_ context.Context, customerID int64) (qdb.MyOrdersSummaryRow, error) {
	var row qdb.MyOrdersSummaryRow
	for _, o := range s.d.orders {
		if o.CustomerID == customerID && o.OrderType == "ecommerce" {
			switch o.Status {
			case "pending_approval", "draft":
				row.PendingCount++
			case "confirmed", "invoiced":
				if o.PaidAmount.LessThan(o.TotalAmount) {
					row.PendingCount++
				} else {
					row.ProcessingCount++
				}
			case "picking", "paid":
				row.ProcessingCount++
			case "shipped":
				row.ShippingCount++
			case "delivered":
				row.DeliveredCount++
			case "cancelled", "rejected":
				row.CancelledCount++
			}
		}
	}
	return row, nil
}
func (s *salesStore) CreateItem(_ context.Context, _ qdb.DBTX, p qdb.CreateSalesOrderItemParams) (int64, error) {
	it := sales.SOItem{
		ID:              s.d.newID(),
		SalesOrderID:    p.SalesOrderID,
		ProductID:       p.ProductID,
		Quantity:        p.Quantity,
		UnitPrice:       p.UnitPrice,
		DiscountPercent: p.DiscountPercent,
		DiscountAmount:  p.DiscountAmount,
		Subtotal:        p.Subtotal,
	}
	s.d.orderIt[p.SalesOrderID] = append(s.d.orderIt[p.SalesOrderID], it)
	return it.ID, nil
}
func (s *salesStore) ItemsByOrder(_ context.Context, orderID int64) ([]qdb.ListSalesOrderItemsRow, error) {
	items := s.d.orderIt[orderID]
	out := make([]qdb.ListSalesOrderItemsRow, 0, len(items))
	for _, it := range items {
		out = append(out, salesItemRow(it))
	}
	return out, nil
}
func (s *salesStore) DeleteItems(_ context.Context, _ qdb.DBTX, orderID int64) error {
	delete(s.d.orderIt, orderID)
	return nil
}
func (s *salesStore) ReturnedQtyByOrder(_ context.Context, _ int64) (map[int64]decimal.Decimal, error) {
	return map[int64]decimal.Decimal{}, nil
}
func (s *salesStore) SetOrderStatus(_ context.Context, _ qdb.DBTX, id int64, status string) error {
	if o, ok := s.d.orders[id]; ok {
		o.Status = status
	}
	return nil
}

func (s *salesStore) SetReceivedProofURL(_ context.Context, _ qdb.DBTX, id int64, url string) error {
	if o, ok := s.d.orders[id]; ok {
		o.ReceivedProofURL = &url
	}
	return nil
}
func (s *salesStore) SettleOrderPayment(_ context.Context, _ qdb.DBTX, orderID int64, amount decimal.Decimal) error {
	o, ok := s.d.orders[orderID]
	if !ok {
		return sales.ErrNotFound
	}
	o.PaidAmount = o.PaidAmount.Add(amount)
	if o.PaidAmount.GreaterThan(o.TotalAmount) {
		o.PaidAmount = o.TotalAmount
	}
	rem := o.RemainingAmount.Sub(amount)
	if rem.IsNegative() {
		rem = decimal.Zero
	}
	o.RemainingAmount = rem
	if (o.Status == "draft" || o.Status == "pending_approval" || o.Status == "confirmed") && o.RemainingAmount.IsZero() {
		o.Status = "invoiced"
	}
	return nil
}
func (s *salesStore) UpdateOrderNotes(_ context.Context, _ qdb.DBTX, id int64, notes pgtype.Text) error {
	o, ok := s.d.orders[id]
	if !ok {
		return sales.ErrNotFound
	}
	o.Notes = pgtzTextToStrPtr(notes)
	return nil
}
func (s *salesStore) SetSalesOrderShift(_ context.Context, _ qdb.DBTX, _, _ int64) error {
	return nil
}
func (s *salesStore) ListSupervisors(_ context.Context) ([]sales.SupervisorOption, error) {
	return []sales.SupervisorOption{
		{ID: 1, Name: "Admin User", Role: "admin", RoleLabel: "Administrator"},
	}, nil
}
func stockRowID(productID, warehouseID int64) int64 { return productID*1000000 + warehouseID }

func (s *salesStore) LockStockForUpdate(_ context.Context, _ qdb.DBTX, productID, warehouseID int64) (qdb.GetSalesStockRowForUpdateRow, error) {
	k := stockKey(productID, warehouseID)
	if _, ok := s.d.stock[k]; !ok {
		return qdb.GetSalesStockRowForUpdateRow{}, sales.ErrNotFound
	}
	return qdb.GetSalesStockRowForUpdateRow{
		ID:               stockRowID(productID, warehouseID),
		ProductID:        productID,
		WarehouseID:      warehouseID,
		Quantity:         s.d.stockQty(productID, warehouseID),
		ReservedQuantity: s.d.reservedQty(productID, warehouseID),
	}, nil
}
func (s *salesStore) EnsureStockRow(_ context.Context, _ qdb.DBTX, productID, warehouseID int64) error {
	if _, ok := s.d.stock[stockKey(productID, warehouseID)]; !ok {
		s.d.setStock(productID, warehouseID, decimal.NewFromInt(1000000))
	}
	return nil
}
func (s *salesStore) SetStockQuantity(_ context.Context, _ qdb.DBTX, id int64, quantity, reserved decimal.Decimal) error {
	productID := id / 1000000
	warehouseID := id % 1000000
	k := stockKey(productID, warehouseID)
	s.d.stock[k] = quantity
	s.d.reserved[k] = reserved
	return nil
}
func (s *salesStore) OpenInvoiceForOrder(_ context.Context, _ qdb.DBTX, orderID int64) (*sales.Invoice, error) {
	for _, inv := range s.d.invoices {
		if inv.SalesOrderID == orderID && (inv.Status == "unpaid" || inv.Status == "partial") {
			return &sales.Invoice{
				ID: inv.ID, InvoiceNumber: inv.InvoiceNumber, SalesOrderID: inv.SalesOrderID,
				CustomerID: inv.CustomerID, InvoiceDate: inv.InvoiceDate, DueDate: inv.DueDate,
				Status: inv.Status, Subtotal: inv.Subtotal, TaxAmount: inv.TaxAmount,
				TotalAmount: inv.TotalAmount, PaidAmount: inv.PaidAmount, RemainingAmount: inv.RemainingAmount,
			}, nil
		}
	}
	return nil, sales.ErrNotFound
}
func (s *salesStore) InvoiceForOrder(_ context.Context, _ qdb.DBTX, orderID int64) (*sales.Invoice, error) {
	for _, inv := range s.d.invoices {
		if inv.SalesOrderID == orderID {
			return &sales.Invoice{
				ID: inv.ID, InvoiceNumber: inv.InvoiceNumber, SalesOrderID: inv.SalesOrderID,
				CustomerID: inv.CustomerID, InvoiceDate: inv.InvoiceDate, DueDate: inv.DueDate,
				Status: inv.Status, Subtotal: inv.Subtotal, TaxAmount: inv.TaxAmount,
				TotalAmount: inv.TotalAmount, PaidAmount: inv.PaidAmount, RemainingAmount: inv.RemainingAmount,
				SnapOrderRef: inv.SnapOrderRef,
			}, nil
		}
	}
	return nil, sales.ErrNotFound
}
func (s *salesStore) InvoiceExistsForSO(_ context.Context, _ qdb.DBTX, soID int64) (bool, error) {
	for _, inv := range s.d.invoices {
		if inv.SalesOrderID == soID {
			return true, nil
		}
	}
	return false, nil
}
func (s *salesStore) SetInvoiceSnapRef(_ context.Context, _ qdb.DBTX, id int64, ref string) error {
	if inv, ok := s.d.invoices[id]; ok {
		inv.SnapOrderRef = &ref
	}
	return nil
}

func (s *salesStore) InvoiceBySnapRef(_ context.Context, _ qdb.DBTX, ref string) (*sales.Invoice, error) {
	for _, inv := range s.d.invoices {
		if inv.SnapOrderRef != nil && (*inv.SnapOrderRef == ref || strings.HasPrefix(*inv.SnapOrderRef, ref+"-")) {
			return &sales.Invoice{
				ID: inv.ID, InvoiceNumber: inv.InvoiceNumber, SalesOrderID: inv.SalesOrderID,
				CustomerID: inv.CustomerID, InvoiceDate: inv.InvoiceDate, DueDate: inv.DueDate,
				Status: inv.Status, Subtotal: inv.Subtotal, TaxAmount: inv.TaxAmount,
				TotalAmount: inv.TotalAmount, PaidAmount: inv.PaidAmount, RemainingAmount: inv.RemainingAmount,
				SnapOrderRef: inv.SnapOrderRef,
			}, nil
		}
	}
	return nil, sales.ErrNotFound
}
func (s *salesStore) MarkInvoicePaid(_ context.Context, _ qdb.DBTX, id int64, paid decimal.Decimal) (int64, error) {
	inv, ok := s.d.invoices[id]
	if !ok {
		return 0, nil
	}
	inv.Status, inv.PaidAmount, inv.RemainingAmount = "paid", paid, decimal.Zero
	return 1, nil
}

// The parity ledger's Invoice is finance.Invoice, which carries no
// snap_last_checked_at: the stamp is reconciler bookkeeping and no parity
// workflow asserts on it. Recorded on the shared row's map is unnecessary, so
// this mirrors SetInvoiceSnapRef's shape: no-op, nil.
func (s *salesStore) SetInvoiceSnapChecked(context.Context, qdb.DBTX, int64) error {
	return nil
}

// Added with the POS QRIS Core API work (backend 1caa533) and missed here, so
// internal/parity stopped compiling. A test package that does not build takes
// every other parity test down with it, which is how a build break goes
// unnoticed for a whole release: nothing is red on the pages people look at.
//
// transactionID and expiresAt are accepted and ignored: they land in
// invoices.gateway_transaction_id / gateway_expires_at (migration 000118), which
// live on the DB row and not on finance.Invoice. The fake has no column for
// them, and inventing one here would be a field nothing reads.
func (s *salesStore) SetInvoiceGatewayRef(_ context.Context, _ qdb.DBTX, invoiceID int64, ref string, _ *string, _ *time.Time) error {
	inv, ok := s.d.invoices[invoiceID]
	if !ok {
		return nil
	}
	inv.SnapOrderRef = &ref
	return nil
}
func (s *salesStore) ListSnapSweepCandidates(context.Context, int) ([]sales.SnapSweepCandidate, error) {
	return nil, nil
}
func (s *salesStore) CancelOrderInvoices(_ context.Context, _ qdb.DBTX, orderID int64) error {
	for _, inv := range s.d.invoices {
		if inv.SalesOrderID == orderID {
			inv.Status = "cancelled"
			inv.RemainingAmount = decimal.Zero
		}
	}
	return nil
}
func (s *salesStore) CreateInvoice(_ context.Context, _ qdb.DBTX, p qdb.CreateSalesInvoiceParams) (int64, error) {
	id := s.d.newID()
	s.d.invoices[id] = &finance.Invoice{
		ID:              id,
		InvoiceNumber:   p.InvoiceNumber,
		SalesOrderID:    p.SalesOrderID.Int64,
		CustomerID:      p.CustomerID,
		InvoiceDate:     pgtzDateToStr(p.InvoiceDate),
		DueDate:         pgtzDateToStr(p.DueDate),
		Status:          p.Status,
		Subtotal:        p.Subtotal,
		TaxAmount:       p.TaxAmount,
		TotalAmount:     p.TotalAmount,
		PaidAmount:      p.PaidAmount,
		RemainingAmount: p.RemainingAmount,
	}
	return id, nil
}
func (s *salesStore) CreatePayment(_ context.Context, _ qdb.DBTX, p qdb.CreateSalesPaymentParams) (int64, error) {
	id := s.d.newID()
	s.d.payments[id] = &finance.Payment{
		ID:            id,
		PaymentNumber: p.PaymentNumber,
		PayableType:   p.PayableType,
		PayableID:     p.PayableID,
		PaymentDate:   pgtzDateToStr(p.PaymentDate),
		Amount:        p.Amount,
		PaymentMethod: p.PaymentMethod,
	}
	return id, nil
}
func (s *salesStore) CreateGatewayPayment(ctx context.Context, q qdb.DBTX, p qdb.CreateSalesPaymentParams, gateway, txnID, status string) (int64, error) {
	return s.CreatePayment(ctx, q, p)
}
func (s *salesStore) CustomerExists(_ context.Context, _ qdb.DBTX, id int64) (bool, error) {
	return s.d.customer[id], nil
}
func (s *salesStore) ProductExists(_ context.Context, _ qdb.DBTX, id int64) (bool, error) {
	return s.d.product[id], nil
}
func (s *salesStore) FirstActiveWarehouse(context.Context, qdb.DBTX) (int64, error) { return 1, nil }
func (s *salesStore) WarehouseForStore(context.Context, qdb.DBTX, int64) (int64, error) {
	return 1, nil
}
func (s *salesStore) FirstActiveStore(context.Context, qdb.DBTX) (int64, int64, bool, error) {
	return 0, 0, false, nil
}
func (s *salesStore) IsStoreActive(context.Context, qdb.DBTX, int64) (bool, error) {
	return true, nil
}
func (s *salesStore) IsUserInActiveStore(context.Context, qdb.DBTX, int64, int64) (bool, error) {
	return true, nil
}
func (s *salesStore) ActiveStoresForUser(context.Context, qdb.DBTX, int64) ([]int64, error) {
	return []int64{1}, nil
}
func (s *salesStore) ShiftStoreID(context.Context, qdb.DBTX, int64) (*int64, error) {
	id := int64(1)
	return &id, nil
}
func (s *salesStore) SetShiftStoreID(context.Context, qdb.DBTX, int64, int64) error {
	return nil
}
func (s *salesStore) ValidateCheckoutWarehouse(_ context.Context, _ qdb.DBTX, warehouseID int64) error {
	if warehouseID != 1 {
		return fmt.Errorf("%w: choose an active warehouse", sales.ErrCheckoutValidation)
	}
	return nil
}

func (s *salesStore) ProductSellingPrice(_ context.Context, _ qdb.DBTX, id int64) (decimal.Decimal, error) {
	return s.d.price(id), nil
}
func (s *salesStore) CheckStockAvailable(_ context.Context, _ qdb.DBTX, productID, warehouseID int64, qty decimal.Decimal) (bool, error) {
	return s.d.stockQty(productID, warehouseID).Sub(s.d.reservedQty(productID, warehouseID)).GreaterThanOrEqual(qty), nil
}
func (s *salesStore) CustomerByUserID(_ context.Context, _ qdb.DBTX, userID int64) (int64, error) {
	if id, ok := s.d.custByUser[userID]; ok {
		return id, nil
	}
	return 0, sales.ErrNotFound
}
func (s *salesStore) CustomerAssignedSalesID(_ context.Context, _ qdb.DBTX, customerID int64) (*int64, error) {
	return nil, nil
}

func (s *salesStore) CreateCustomerForUser(_ context.Context, _ qdb.DBTX, code, name, email string, userID int64) (int64, error) {
	if id, ok := s.d.custByUser[userID]; ok {
		return id, nil
	}
	id := s.d.newID()
	s.d.custByUser[userID] = id
	s.d.customer[id] = true
	return id, nil
}
func (s *salesStore) WalkInCustomerID(_ context.Context, _ qdb.DBTX) (int64, error) {
	return 1, nil
}

// --- sales vouchers (unused by parity workflows) ----------------------------

func (s *salesStore) CreateSalesVoucher(_ context.Context, _ qdb.DBTX, _ qdb.CreateSalesVoucherParams) (qdb.CreateSalesVoucherRow, error) {
	return qdb.CreateSalesVoucherRow{}, sales.ErrNotFound
}
func (s *salesStore) GetSalesVoucherByID(_ context.Context, _ qdb.DBTX, _ int64) (qdb.GetSalesVoucherByIDRow, error) {
	return qdb.GetSalesVoucherByIDRow{}, sales.ErrNotFound
}
func (s *salesStore) GetSalesVoucherByIDForUpdate(_ context.Context, _ qdb.DBTX, _ int64) (qdb.GetSalesVoucherByIDForUpdateRow, error) {
	return qdb.GetSalesVoucherByIDForUpdateRow{}, sales.ErrNotFound
}
func (s *salesStore) GetSalesVoucherByNumber(_ context.Context, _ qdb.DBTX, _ string) (qdb.GetSalesVoucherByNumberRow, error) {
	return qdb.GetSalesVoucherByNumberRow{}, sales.ErrNotFound
}
func (s *salesStore) GetSalesVoucherByNumberForUpdate(_ context.Context, _ qdb.DBTX, _ string) (qdb.GetSalesVoucherByNumberForUpdateRow, error) {
	return qdb.GetSalesVoucherByNumberForUpdateRow{}, sales.ErrNotFound
}
func (s *salesStore) UpdateSalesVoucherStatus(_ context.Context, _ qdb.DBTX, _ int64, _ string) error {
	return sales.ErrNotFound
}
func (s *salesStore) ListSalesVouchers(_ context.Context, _ qdb.DBTX, _ qdb.ListSalesVouchersParams) ([]qdb.ListSalesVouchersRow, error) {
	return nil, nil
}
func (s *salesStore) CountSalesVouchers(_ context.Context, _ qdb.DBTX, _ qdb.CountSalesVouchersParams) (int64, error) {
	return 0, nil
}

// --- sales returns (unused by parity workflows) ------------------------------

func (s *salesStore) CreateSalesReturn(_ context.Context, _ qdb.DBTX, _ qdb.CreateSalesReturnParams) (qdb.CreateSalesReturnRow, error) {
	return qdb.CreateSalesReturnRow{}, sales.ErrNotFound
}
func (s *salesStore) GetSalesReturnByID(_ context.Context, _ qdb.DBTX, _ int64) (qdb.GetSalesReturnByIDRow, error) {
	return qdb.GetSalesReturnByIDRow{}, sales.ErrNotFound
}
func (s *salesStore) ListSalesReturns(_ context.Context, _ qdb.DBTX, _ qdb.ListSalesReturnsParams) ([]qdb.ListSalesReturnsRow, error) {
	return nil, nil
}
func (s *salesStore) CountSalesReturns(_ context.Context, _ qdb.DBTX, _ qdb.CountSalesReturnsParams) (int64, error) {
	return 0, nil
}
func (s *salesStore) CreateSalesReturnItem(_ context.Context, _ qdb.DBTX, _ qdb.CreateSalesReturnItemParams) (qdb.SalesReturnItem, error) {
	return qdb.SalesReturnItem{}, sales.ErrNotFound
}
func (s *salesStore) SetSalesReturnItemCondition(_ context.Context, _ qdb.DBTX, _ qdb.SetSalesReturnItemConditionParams) (qdb.SalesReturnItem, error) {
	return qdb.SalesReturnItem{}, nil
}
func (s *salesStore) ListSalesReturnItems(_ context.Context, _ qdb.DBTX, _ int64) ([]qdb.ListSalesReturnItemsRow, error) {
	return nil, nil
}
func (s *salesStore) SumReturnedQtyByOrderItem(_ context.Context, _ qdb.DBTX, _ int64) (decimal.Decimal, error) {
	return decimal.Zero, nil
}

// --- helpers ---------------------------------------------------------------

func int8OrNil(p pgtype.Int8) *int64 {
	if !p.Valid {
		return nil
	}
	v := p.Int64
	return &v
}

func int8Ptr(p *int64) pgtype.Int8 {
	if p == nil {
		return pgtype.Int8{}
	}
	return pgtype.Int8{Int64: *p, Valid: true}
}

func pgtzTextToStrPtr(p pgtype.Text) *string {
	if !p.Valid {
		return nil
	}
	v := p.String
	return &v
}

func textPtr(p *string) pgtype.Text {
	if p == nil {
		return pgtype.Text{}
	}
	return pgtype.Text{String: *p, Valid: true}
}

func pgtzDateToStr(p pgtype.Date) string {
	if !p.Valid {
		return ""
	}
	return p.Time.Format("2006-01-02")
}

func pgtzDateToStrPtr(p pgtype.Date) *string {
	if !p.Valid {
		return nil
	}
	v := p.Time.Format("2006-01-02")
	return &v
}

func datePtr(p *string) pgtype.Date {
	if p == nil {
		return pgtype.Date{}
	}
	t, err := time.Parse("2006-01-02", *p)
	if err != nil {
		return pgtype.Date{}
	}
	return pgtype.Date{Time: t, Valid: true}
}

func tsToPg(t time.Time) pgtype.Timestamptz {
	return pgtype.Timestamptz{Time: t, Valid: true}
}

func salesOrderRow(o *sales.SalesOrder) qdb.GetSalesOrderByIDRow {
	return qdb.GetSalesOrderByIDRow{
		ID:              o.ID,
		SoNumber:        o.SoNumber,
		CustomerID:      o.CustomerID,
		WarehouseID:     int8Ptr(o.WarehouseID),
		SalesID:         int8Ptr(o.SalesID),
		OrderDate:       o.OrderDate,
		Status:          o.Status,
		OrderType:       o.OrderType,
		Subtotal:        o.Subtotal,
		TaxAmount:       o.TaxAmount,
		DiscountAmount:  o.DiscountAmount,
		TotalAmount:     o.TotalAmount,
		PaidAmount:      o.PaidAmount,
		RemainingAmount: o.RemainingAmount,
		ShippingCost:    o.ShippingCost,
		ShippingAddress: textPtr(o.ShippingAddress),
		Notes:           textPtr(o.Notes),
		DeliveryDate:    datePtr(o.DeliveryDate),
		CreatedAt:       tsToPg(o.CreatedAt),
		UpdatedAt:       tsToPg(o.UpdatedAt),
	}
}

func salesListOrderRow(o *sales.SalesOrder) qdb.ListSalesOrdersRow {
	r := salesOrderRow(o)
	return qdb.ListSalesOrdersRow{
		ID: r.ID, SoNumber: r.SoNumber, CustomerID: r.CustomerID, WarehouseID: r.WarehouseID,
		SalesID: r.SalesID, StoreID: r.StoreID, OrderDate: r.OrderDate, Status: r.Status,
		OrderType: r.OrderType, SourceType: r.SourceType, Subtotal: r.Subtotal, TaxAmount: r.TaxAmount,
		DiscountAmount: r.DiscountAmount, TotalAmount: r.TotalAmount, PaidAmount: r.PaidAmount,
		RemainingAmount: r.RemainingAmount, ShippingCost: r.ShippingCost, ShippingAddress: r.ShippingAddress,
		Notes: r.Notes, DeliveryDate: r.DeliveryDate, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
		CustomerName: r.CustomerName, SalesName: r.SalesName, WarehouseName: r.WarehouseName,
	}
}

func salesItemRow(it sales.SOItem) qdb.ListSalesOrderItemsRow {
	return qdb.ListSalesOrderItemsRow{
		ID:              it.ID,
		SalesOrderID:    it.SalesOrderID,
		ProductID:       it.ProductID,
		Quantity:        it.Quantity,
		UnitPrice:       it.UnitPrice,
		DiscountPercent: it.DiscountPercent,
		DiscountAmount:  it.DiscountAmount,
		Subtotal:        it.Subtotal,
		Code:            it.ProductCode,
		Name:            it.ProductName,
	}
}

// --- purchasing.Store --------------------------------------------------

type purchStore struct{ d *db }

func (s *purchStore) Begin(context.Context) (purchasing.Tx, error) { return fakeTx{}, nil }
func (s *purchStore) CreateOrder(_ context.Context, _ qdb.DBTX, p qdb.CreatePurchaseOrderParams) (qdb.CreatePurchaseOrderRow, error) {
	now := time.Now()
	o := &purchasing.PurchaseOrder{
		ID: s.d.newID(), PoNumber: p.PoNumber, SupplierID: p.SupplierID, WarehouseID: p.WarehouseID,
		CreatedBy: int8OrNil(p.CreatedBy), OrderDate: pgtzDateToStr(p.OrderDate), ExpectedDate: pgtzDateToStrPtr(p.ExpectedDate),
		DueDate: pgtzDateToStrPtr(p.DueDate), Status: p.Status, Subtotal: p.Subtotal, TaxAmount: p.TaxAmount,
		DiscountAmount: p.DiscountAmount, TotalAmount: p.TotalAmount, PaidAmount: p.PaidAmount,
		RemainingAmount: p.RemainingAmount, Notes: pgtzTextToStrPtr(p.Notes), CreatedAt: now, UpdatedAt: now,
	}
	s.d.pos[o.ID] = o
	return qdb.CreatePurchaseOrderRow{ID: o.ID, CreatedAt: tsToPg(now), UpdatedAt: tsToPg(now)}, nil
}
func (s *purchStore) UpdateOrder(_ context.Context, _ qdb.DBTX, p qdb.UpdatePurchaseOrderParams) error {
	o := s.d.pos[p.ID]
	o.PoNumber = p.PoNumber
	o.SupplierID = p.SupplierID
	o.WarehouseID = p.WarehouseID
	o.OrderDate = pgtzDateToStr(p.OrderDate)
	o.ExpectedDate = pgtzDateToStrPtr(p.ExpectedDate)
	o.DueDate = pgtzDateToStrPtr(p.DueDate)
	o.Status = p.Status
	o.Subtotal = p.Subtotal
	o.TaxAmount = p.TaxAmount
	o.DiscountAmount = p.DiscountAmount
	o.TotalAmount = p.TotalAmount
	o.PaidAmount = p.PaidAmount
	o.RemainingAmount = p.RemainingAmount
	o.Notes = pgtzTextToStrPtr(p.Notes)
	o.UpdatedAt = time.Now()
	return nil
}
func (s *purchStore) DeleteOrder(_ context.Context, _ qdb.DBTX, id int64) error {
	delete(s.d.pos, id)
	delete(s.d.poIt, id)
	return nil
}
func (s *purchStore) StampPrint(_ context.Context, p qdb.PurchaseOrdersStampPrintParams) (int64, error) {
	o, ok := s.d.pos[p.ID]
	if !ok {
		return 0, nil
	}
	if o.PrintCount > 0 && !p.AllowReprint {
		return 0, nil
	}
	o.PrintCount++
	now := time.Now()
	o.PrintedAt = &now
	return 1, nil
}
func (s *purchStore) OrderByID(_ context.Context, id int64) (qdb.GetPurchaseOrderByIDRow, error) {
	o, ok := s.d.pos[id]
	if !ok {
		return qdb.GetPurchaseOrderByIDRow{}, purchasing.ErrNotFound
	}
	return purchaseOrderRow(o), nil
}
func (s *purchStore) ApproveOrder(_ context.Context, _ qdb.DBTX, p qdb.ApprovePurchaseOrderParams) error {
	if o, ok := s.d.pos[p.ID]; ok {
		o.Status = "approved"
	}
	return nil
}

func (s *purchStore) OrderByIDForUpdate(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetPurchaseOrderByIDForUpdateRow, error) {
	o, ok := s.d.pos[id]
	if !ok {
		return qdb.GetPurchaseOrderByIDForUpdateRow{}, purchasing.ErrNotFound
	}
	return qdb.GetPurchaseOrderByIDForUpdateRow{
		ID: o.ID, PoNumber: o.PoNumber, SupplierID: o.SupplierID,
		WarehouseID: o.WarehouseID, CreatedBy: int8Ptr(o.CreatedBy), OrderDate: o.OrderDate,
		ExpectedDate: stringOrEmpty(o.ExpectedDate), DueDate: stringOrEmpty(o.DueDate), Status: o.Status,
		Subtotal: o.Subtotal, TaxAmount: o.TaxAmount, DiscountAmount: o.DiscountAmount,
		TotalAmount: o.TotalAmount, PaidAmount: o.PaidAmount, RemainingAmount: o.RemainingAmount,
		Notes: textPtr(o.Notes), CreatedAt: tsToPg(o.CreatedAt), UpdatedAt: tsToPg(o.UpdatedAt)}, nil
}
func (s *purchStore) ListOrders(context.Context, purchasing.POFilter) ([]qdb.ListPurchaseOrdersRow, int, error) {
	return nil, 0, nil
}
func (s *purchStore) CreateItem(_ context.Context, _ qdb.DBTX, p qdb.CreatePurchaseOrderItemParams) (int64, error) {
	it := purchasing.POItem{ID: s.d.newID(), PurchaseOrderID: p.PurchaseOrderID, ProductID: p.ProductID,
		Quantity: p.Quantity, UnitPrice: p.UnitPrice, Discount: p.Discount, Subtotal: p.Subtotal}
	s.d.poIt[it.PurchaseOrderID] = append(s.d.poIt[it.PurchaseOrderID], it)
	return it.ID, nil
}
func (s *purchStore) ItemsByOrder(_ context.Context, orderID int64) ([]qdb.ListPurchaseOrderItemsRow, error) {
	out := make([]qdb.ListPurchaseOrderItemsRow, 0, len(s.d.poIt[orderID]))
	for _, it := range s.d.poIt[orderID] {
		out = append(out, qdb.ListPurchaseOrderItemsRow{ID: it.ID, PurchaseOrderID: it.PurchaseOrderID,
			ProductID: it.ProductID, Quantity: it.Quantity, ReceivedQuantity: it.ReceivedQuantity,
			UnitPrice: it.UnitPrice, Discount: it.Discount, Subtotal: it.Subtotal,
			Code: it.ProductCode, Name: it.ProductName})
	}
	return out, nil
}
func (s *purchStore) ItemsByOrderForUpdate(_ context.Context, _ qdb.DBTX, orderID int64) ([]qdb.ListPurchaseOrderItemsForUpdateRow, error) {
	items, err := s.ItemsByOrder(context.Background(), orderID)
	if err != nil {
		return nil, err
	}
	out := make([]qdb.ListPurchaseOrderItemsForUpdateRow, 0, len(items))
	for _, it := range items {
		out = append(out, qdb.ListPurchaseOrderItemsForUpdateRow(it))
	}
	return out, nil
}
func (s *purchStore) DeleteItems(_ context.Context, _ qdb.DBTX, orderID int64) error {
	delete(s.d.poIt, orderID)
	return nil
}
func (s *purchStore) SetOrderStatus(_ context.Context, _ qdb.DBTX, p qdb.SetPurchaseOrderStatusParams) error {
	if o, ok := s.d.pos[p.ID]; ok {
		o.Status = p.Status
	}
	return nil
}
func (s *purchStore) AddReceived(_ context.Context, _ qdb.DBTX, p qdb.AddReceivedQuantityParams) error {
	for oid := range s.d.poIt {
		for i := range s.d.poIt[oid] {
			if s.d.poIt[oid][i].ID == p.ID {
				s.d.poIt[oid][i].ReceivedQuantity = s.d.poIt[oid][i].ReceivedQuantity.Add(p.ReceivedQuantity)
			}
		}
	}
	return nil
}
func (s *purchStore) CreateGR(_ context.Context, _ qdb.DBTX, p qdb.CreateGoodsReceiptParams) (qdb.CreateGoodsReceiptRow, error) {
	now := time.Now()
	g := &purchasing.GoodsReceipt{ID: s.d.newID(), GrNumber: p.GrNumber, PurchaseOrderID: p.PurchaseOrderID,
		WarehouseID: p.WarehouseID, ReceivedBy: int8OrNil(p.ReceivedBy), ReceivedDate: pgtzDateToStr(p.ReceivedDate),
		Notes: pgtzTextToStrPtr(p.Notes), Status: p.Status, CreatedAt: now}
	s.d.grs[g.ID] = g
	return qdb.CreateGoodsReceiptRow{ID: g.ID, CreatedAt: tsToPg(now)}, nil
}
func (s *purchStore) CreateGRItem(_ context.Context, _ qdb.DBTX, p qdb.CreateGoodsReceiptItemParams) (int64, error) {
	it := purchasing.GRItem{ID: s.d.newID(), GoodsReceiptID: p.GoodsReceiptID,
		PurchaseOrderItemID: p.PurchaseOrderItemID.Int64, ProductID: p.ProductID,
		QuantityOrdered: p.QuantityOrdered, QuantityReceived: p.QuantityReceived,
		BatchNumber: pgtzTextToStrPtr(p.BatchNumber), ExpiredDate: pgtzDateToStrPtr(p.ExpiredDate)}
	s.d.grIt[it.GoodsReceiptID] = append(s.d.grIt[it.GoodsReceiptID], it)
	return it.ID, nil
}
func (s *purchStore) GRByID(_ context.Context, id int64) (qdb.GetGoodsReceiptByIDRow, error) {
	g, ok := s.d.grs[id]
	if !ok {
		return qdb.GetGoodsReceiptByIDRow{}, purchasing.ErrNotFound
	}
	return qdb.GetGoodsReceiptByIDRow{ID: g.ID, GrNumber: g.GrNumber, PurchaseOrderID: g.PurchaseOrderID,
		WarehouseID: g.WarehouseID, ReceivedBy: int8Ptr(g.ReceivedBy), ReceivedDate: g.ReceivedDate,
		Notes: textPtr(g.Notes), Status: g.Status, CreatedAt: tsToPg(g.CreatedAt)}, nil
}
func (s *purchStore) GRItems(_ context.Context, grID int64) ([]qdb.ListGoodsReceiptItemsRow, error) {
	out := make([]qdb.ListGoodsReceiptItemsRow, 0, len(s.d.grIt[grID]))
	for _, it := range s.d.grIt[grID] {
		out = append(out, qdb.ListGoodsReceiptItemsRow{ID: it.ID, GoodsReceiptID: it.GoodsReceiptID,
			PurchaseOrderItemID: pgtype.Int8{Int64: it.PurchaseOrderItemID, Valid: it.PurchaseOrderItemID != 0},
			ProductID:           it.ProductID, QuantityOrdered: it.QuantityOrdered, QuantityReceived: it.QuantityReceived,
			BatchNumber: textPtr(it.BatchNumber), ExpiredDate: stringOrEmpty(it.ExpiredDate),
			Code: it.ProductCode, Name: it.ProductName})
	}
	return out, nil
}
func (s *purchStore) ListGRs(context.Context, purchasing.GRFilter) ([]qdb.ListGoodsReceiptsRow, int, error) {
	return nil, 0, nil
}
func (s *purchStore) ReceiveStock(_ context.Context, _ qdb.DBTX, p qdb.RawStockAddParams, _, _ int64, _ *int64) error {
	s.d.setStock(p.ProductID, p.WarehouseID, p.Quantity)
	return nil
}
func (s *purchStore) ListDebts(context.Context, purchasing.DebtFilter) ([]qdb.ListDebtsRow, int, purchasing.DebtSummary, error) {
	rows := []qdb.ListDebtsRow{}
	var total decimal.Decimal
	for _, po := range s.d.pos {
		if po.Status != "received" && po.Status != "partial" {
			continue
		}
		rows = append(rows, qdb.ListDebtsRow{
			ID: po.ID, PoNumber: po.PoNumber, SupplierID: po.SupplierID, OrderDate: po.OrderDate,
			DueDate: stringOrEmpty(po.DueDate), Status: po.Status, TotalAmount: po.TotalAmount,
			PaidAmount: po.PaidAmount, RemainingAmount: po.RemainingAmount,
		})
		total = total.Add(po.RemainingAmount)
	}
	summary := purchasing.DebtSummary{
		TotalAmount: total,
		CountUnpaid: int64(len(rows)),
	}
	return rows, len(rows), summary, nil
}
func (s *purchStore) CreateVoucher(_ context.Context, _ qdb.DBTX, p qdb.CreateVoucherParams) (int64, error) {
	v := &purchasing.Voucher{ID: s.d.newID(), VoucherNumber: p.VoucherNumber, Type: p.Type,
		Description: pgtzTextToStrPtr(p.Description), VoucherDate: pgtzDateToStr(p.VoucherDate),
		Amount: p.Amount, Status: p.Status, CreatedBy: int8OrNil(p.CreatedBy)}
	s.d.vouchers[v.ID] = v
	return v.ID, nil
}
func (s *purchStore) VoucherByID(_ context.Context, id int64) (qdb.GetVoucherByIDRow, error) {
	v, ok := s.d.vouchers[id]
	if !ok {
		return qdb.GetVoucherByIDRow{}, purchasing.ErrNotFound
	}
	return qdb.GetVoucherByIDRow{ID: v.ID, VoucherNumber: v.VoucherNumber, Type: v.Type,
		Description: textPtr(v.Description), VoucherDate: v.VoucherDate, Amount: v.Amount,
		Status: v.Status, CreatedBy: int8Ptr(v.CreatedBy)}, nil
}
func (s *purchStore) VoucherByIDForUpdate(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetVoucherByIDForUpdateRow, error) {
	v, ok := s.d.vouchers[id]
	if !ok {
		return qdb.GetVoucherByIDForUpdateRow{}, purchasing.ErrNotFound
	}
	return qdb.GetVoucherByIDForUpdateRow{ID: v.ID, VoucherNumber: v.VoucherNumber, Type: v.Type,
		Description: textPtr(v.Description), VoucherDate: v.VoucherDate, Amount: v.Amount,
		Status: v.Status, CreatedBy: int8Ptr(v.CreatedBy)}, nil
}
func (s *purchStore) UpdateVoucherStatus(_ context.Context, _ qdb.DBTX, p qdb.UpdateVoucherStatusParams) error {
	if v, ok := s.d.vouchers[p.ID]; ok {
		if v.Status == "available" {
			v.Status = p.Status
		}
	}
	return nil
}
func (s *purchStore) UpdateVoucherStatusAny(_ context.Context, _ qdb.DBTX, p qdb.UpdateVoucherStatusAnyParams) error {
	if v, ok := s.d.vouchers[p.ID]; ok {
		v.Status = p.Status
	}
	return nil
}
func (s *purchStore) UpdateVoucher(_ context.Context, _ qdb.DBTX, p qdb.UpdateVoucherParams) error {
	if v, ok := s.d.vouchers[p.ID]; ok {
		v.Type = p.Type
		v.Description = pgtzTextToStrPtr(p.Description)
		v.VoucherDate = pgtzDateToStr(p.VoucherDate)
		v.Amount = p.Amount
		v.Status = p.Status
	}
	return nil
}
func (s *purchStore) ListVouchers(context.Context, purchasing.VoucherFilter) ([]qdb.ListVouchersRow, int, error) {
	return nil, 0, nil
}
func (s *purchStore) SupplierExists(_ context.Context, id int64) (bool, error) {
	return s.d.supplier[id], nil
}
func (s *purchStore) WarehouseExists(context.Context, int64) (bool, error) { return true, nil }
func (s *purchStore) ProductExists(_ context.Context, id int64) (bool, error) {
	return s.d.product[id], nil
}
func (s *purchStore) MarkVoucherPrinted(_ context.Context, _ qdb.DBTX, p qdb.MarkVoucherPrintedParams) error {
	v, ok := s.d.vouchers[p.ID]
	if !ok {
		return purchasing.ErrNotFound
	}
	now := time.Now()
	v.PrintedAt = &now
	if p.PrintedBy.Valid {
		id := p.PrintedBy.Int64
		v.PrintedBy = &id
	}
	return nil
}
func (s *purchStore) PurchaseOrderSupplierID(_ context.Context, _ qdb.DBTX, poID int64) (int64, error) {
	o, ok := s.d.pos[poID]
	if !ok {
		return 0, purchasing.ErrNotFound
	}
	return o.SupplierID, nil
}

// --- finance.Store -----------------------------------------------------

type financeStore struct{ d *db }

func (s *financeStore) Begin(context.Context) (finance.Tx, error) { return fakeTx{}, nil }
func (s *financeStore) CreateInvoice(_ context.Context, _ qdb.DBTX, p qdb.CreateInvoiceParams) (qdb.CreateInvoiceRow, error) {
	now := time.Now()
	inv := &finance.Invoice{ID: s.d.newID(), InvoiceNumber: p.InvoiceNumber, SalesOrderID: p.SalesOrderID.Int64,
		CustomerID: p.CustomerID, InvoiceDate: pgtzDateToStr(p.InvoiceDate), DueDate: pgtzDateToStr(p.DueDate),
		Status: p.Status, Subtotal: p.Subtotal, TaxAmount: p.TaxAmount, TotalAmount: p.TotalAmount,
		PaidAmount: p.PaidAmount, RemainingAmount: p.RemainingAmount, Notes: pgtzTextToStrPtr(p.Notes),
		CreatedAt: now, UpdatedAt: now}
	s.d.invoices[inv.ID] = inv
	return qdb.CreateInvoiceRow{ID: inv.ID, CreatedAt: tsToPg(now), UpdatedAt: tsToPg(now)}, nil
}
func (s *financeStore) UpdateInvoice(_ context.Context, _ qdb.DBTX, p qdb.UpdateInvoiceParams) error {
	inv := s.d.invoices[p.ID]
	inv.Status = p.Status
	inv.PaidAmount = p.PaidAmount
	inv.RemainingAmount = p.RemainingAmount
	inv.Notes = pgtzTextToStrPtr(p.Notes)
	inv.UpdatedAt = time.Now()
	return nil
}
func (s *financeStore) InvoiceByID(_ context.Context, id int64) (qdb.GetInvoiceByIDRow, error) {
	inv, ok := s.d.invoices[id]
	if !ok {
		return qdb.GetInvoiceByIDRow{}, finance.ErrNotFound
	}
	return invoiceRow(inv), nil
}
func (s *financeStore) LockInvoice(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetInvoiceByIDForUpdateRow, error) {
	inv, ok := s.d.invoices[id]
	if !ok {
		return qdb.GetInvoiceByIDForUpdateRow{}, finance.ErrNotFound
	}
	return qdb.GetInvoiceByIDForUpdateRow(invoiceRow(inv)), nil
}
func (s *financeStore) ListInvoices(context.Context, finance.InvoiceFilter) ([]qdb.ListInvoicesRow, int, error) {
	return nil, 0, nil
}
func (s *financeStore) InvoiceItems(_ context.Context, salesOrderID int64) ([]qdb.ListInvoiceItemsRow, error) {
	return nil, nil
}
func (s *financeStore) SOByID(_ context.Context, id int64) (qdb.GetSOHeaderByIDRow, error) {
	o, ok := s.d.orders[id]
	if !ok {
		return qdb.GetSOHeaderByIDRow{}, finance.ErrNotFound
	}
	return qdb.GetSOHeaderByIDRow{
		ID:          o.ID,
		SoNumber:    o.SoNumber,
		CustomerID:  o.CustomerID,
		Status:      o.Status,
		TotalAmount: o.TotalAmount,
	}, nil
}
func (s *financeStore) LockSO(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetSOHeaderByIDForUpdateRow, error) {
	o, ok := s.d.orders[id]
	if !ok {
		return qdb.GetSOHeaderByIDForUpdateRow{}, finance.ErrNotFound
	}
	return qdb.GetSOHeaderByIDForUpdateRow{
		ID: o.ID, SoNumber: o.SoNumber, CustomerID: o.CustomerID,
		Status: o.Status, TotalAmount: o.TotalAmount,
	}, nil
}
func (s *financeStore) InvoiceExistsForSO(_ context.Context, _ qdb.DBTX, soID int64) (bool, error) {
	for _, inv := range s.d.invoices {
		if inv.SalesOrderID == soID {
			return true, nil
		}
	}
	return false, nil
}
func (s *financeStore) SetSOStatus(_ context.Context, _ qdb.DBTX, p qdb.SetSOStatusParams) error {
	if o, ok := s.d.orders[p.ID]; ok {
		o.Status = p.Status
	}
	return nil
}
func (s *financeStore) InsertSOStatusHistory(_ context.Context, _ qdb.DBTX, p qdb.InsertSOStatusHistoryParams) error {
	s.d.soHistory = append(s.d.soHistory, qdb.SalesOrderStatusHistory{
		ID: int64(len(s.d.soHistory) + 1), SalesOrderID: p.SalesOrderID,
		FromStatus: p.FromStatus, ToStatus: p.ToStatus, Reason: p.Reason,
		ReferenceType: p.ReferenceType, ReferenceID: p.ReferenceID, ChangedBy: p.ChangedBy,
	})
	return nil
}
func (s *financeStore) InvoiceCreationStatusHistory(_ context.Context, _ qdb.DBTX, p qdb.GetInvoiceCreationStatusHistoryForUpdateParams) (qdb.SalesOrderStatusHistory, error) {
	for i := len(s.d.soHistory) - 1; i >= 0; i-- {
		h := s.d.soHistory[i]
		if h.SalesOrderID == p.SalesOrderID && h.Reason == "invoice_created" && h.ToStatus == "invoiced" &&
			h.ReferenceType.Valid && h.ReferenceType.String == finance.InvoiceModel && h.ReferenceID == p.ReferenceID {
			return h, nil
		}
	}
	return qdb.SalesOrderStatusHistory{}, finance.ErrInvoiceCreationHistoryRequired
}
func (s *financeStore) PaymentsExistForInvoice(_ context.Context, _ qdb.DBTX, invoiceID int64) (bool, error) {
	for _, payment := range s.d.payments {
		if payment.PayableType == finance.InvoiceModel && payment.PayableID == invoiceID {
			return true, nil
		}
	}
	return false, nil
}
func (s *financeStore) DeleteInvoice(_ context.Context, _ qdb.DBTX, id int64) error {
	if _, ok := s.d.invoices[id]; !ok {
		return finance.ErrNotFound
	}
	delete(s.d.invoices, id)
	return nil
}
func (s *financeStore) ListReceivables(context.Context, finance.ReceivableFilter) ([]qdb.ListReceivablesRow, int, decimal.Decimal, error) {
	return nil, 0, decimal.Zero, nil
}
func (s *financeStore) CreatePayment(_ context.Context, _ qdb.DBTX, p qdb.CreatePaymentParams) (int64, error) {
	payment := &finance.Payment{ID: s.d.newID(), PaymentNumber: p.PaymentNumber, PayableType: p.PayableType,
		PayableID: p.PayableID, PaymentDate: pgtzDateToStr(p.PaymentDate), Amount: p.Amount,
		PaymentMethod: p.PaymentMethod, BankName: pgtzTextToStrPtr(p.BankName), BankAccount: pgtzTextToStrPtr(p.BankAccount),
		ReferenceNumber: pgtzTextToStrPtr(p.ReferenceNumber), Notes: pgtzTextToStrPtr(p.Notes), CreatedBy: int8OrNil(p.CreatedBy)}
	s.d.payments[payment.ID] = payment
	return payment.ID, nil
}
func (s *financeStore) ListPayments(context.Context, finance.InvoiceFilter) ([]qdb.ListPaymentsRow, int, error) {
	return nil, 0, nil
}
func (s *financeStore) SumCustomerPayments(context.Context, int64, string, string) (qdb.SumCustomerPaymentsRow, error) {
	return qdb.SumCustomerPaymentsRow{}, nil
}
func (s *financeStore) GetPaymentByID(_ context.Context, id int64) (qdb.GetPaymentByIDForUpdateRow, error) {
	p, ok := s.d.payments[id]
	if !ok {
		return qdb.GetPaymentByIDForUpdateRow{}, finance.ErrNotFound
	}
	return paymentDetailRow(p), nil
}
func (s *financeStore) LockPayment(_ context.Context, _ qdb.DBTX, id int64) (qdb.GetPaymentByIDForUpdateRow, error) {
	return s.GetPaymentByID(context.Background(), id)
}
func (s *financeStore) SetPaymentProofURL(_ context.Context, id int64, url string) (int64, error) {
	p, ok := s.d.payments[id]
	if !ok {
		return 0, nil
	}
	p.ProofURL = &url
	return 1, nil
}
func (s *financeStore) SetPaymentReview(_ context.Context, id int64, verdict string) (int64, error) {
	p, ok := s.d.payments[id]
	if !ok {
		return 0, nil
	}
	p.Notes = &verdict
	return 1, nil
}
func (s *financeStore) PaymentProofOwnersByKey(_ context.Context, key string) ([]int64, bool, error) {
	var owners []int64
	found := false
	for _, p := range s.d.payments {
		if p.ProofURL == nil || (*p.ProofURL != key && *p.ProofURL != "/uploads/"+key) {
			continue
		}
		found = true
	}
	if !found {
		return nil, false, nil
	}
	return owners, true, nil
}
func (s *financeStore) CreateCashflow(_ context.Context, _ qdb.DBTX, p qdb.CreateCashflowParams) (int64, error) {
	c := &finance.Cashflow{ID: s.d.newID(), TransactionNumber: p.TransactionNumber, Type: p.Type,
		Category: p.Category, TransactionDate: pgtzDateToStr(p.TransactionDate), Amount: p.Amount,
		Description: pgtzTextToStrPtr(p.Description), ReferenceType: pgtzTextToStrPtr(p.ReferenceType),
		ReferenceID: int8OrNil(p.ReferenceID), CreatedBy: int8OrNil(p.CreatedBy)}
	s.d.cashflow[c.ID] = c
	return c.ID, nil
}

// CashflowByID / LockCashflow apply the outlet scope (migration 000104) with the
// query's own nil-vs-empty semantics: nil storeIDs is platform-wide and matches
// company-level rows, a non-nil slice matches only those stores and never a
// NULL outlet. A row outside the scope reads as ErrNotFound so the fake cannot
// produce a distinguishable error the real query cannot.
func (s *financeStore) CashflowByID(_ context.Context, id int64, storeIDs []int64) (qdb.GetCashflowByIDRow, error) {
	c, ok := s.d.cashflow[id]
	if !ok || !cashflowInScope(c, storeIDs) {
		return qdb.GetCashflowByIDRow{}, finance.ErrNotFound
	}
	return cashflowRow(c), nil
}
func (s *financeStore) LockCashflow(_ context.Context, _ qdb.DBTX, id int64, storeIDs []int64) (qdb.GetCashflowByIDForUpdateRow, error) {
	row, err := s.CashflowByID(context.Background(), id, storeIDs)
	return qdb.GetCashflowByIDForUpdateRow(row), err
}

func cashflowInScope(c *finance.Cashflow, storeIDs []int64) bool {
	if storeIDs == nil {
		return true
	}
	if c.StoreID == nil {
		return false
	}
	for _, id := range storeIDs {
		if id == *c.StoreID {
			return true
		}
	}
	return false
}
func (s *financeStore) CashflowAlreadyReversed(_ context.Context, _ qdb.DBTX, _ int64) (bool, error) {
	return false, nil
}
func (s *financeStore) UpdateCashflow(_ context.Context, _ qdb.DBTX, p qdb.UpdateCashflowParams) (qdb.UpdateCashflowRow, error) {
	c, ok := s.d.cashflow[p.ID]
	if !ok {
		return qdb.UpdateCashflowRow{}, finance.ErrNotFound
	}
	if c.ReferenceType != nil || c.ReferenceID != nil {
		return qdb.UpdateCashflowRow{}, finance.ErrGeneratedCashflowImmutable
	}
	c.Type = p.Type
	c.Category = p.Category
	c.TransactionDate = pgtzDateToStr(p.TransactionDate)
	c.Amount = p.Amount
	c.Description = pgtzTextToStrPtr(p.Description)
	return qdb.UpdateCashflowRow(cashflowRow(c)), nil
}
func (s *financeStore) DeleteCashflow(_ context.Context, _ qdb.DBTX, id int64) error {
	delete(s.d.cashflow, id)
	return nil
}
func (s *financeStore) ListCashflows(context.Context, finance.CashflowFilter) ([]qdb.ListCashflowsRow, int, qdb.SummarizeCashflowsRow, error) {
	return nil, 0, qdb.SummarizeCashflowsRow{}, nil
}

func purchaseOrderRow(o *purchasing.PurchaseOrder) qdb.GetPurchaseOrderByIDRow {
	return qdb.GetPurchaseOrderByIDRow{ID: o.ID, PoNumber: o.PoNumber, SupplierID: o.SupplierID,
		WarehouseID: o.WarehouseID, CreatedBy: int8Ptr(o.CreatedBy), OrderDate: o.OrderDate,
		ExpectedDate: stringOrEmpty(o.ExpectedDate), DueDate: stringOrEmpty(o.DueDate), Status: o.Status,
		Subtotal: o.Subtotal, TaxAmount: o.TaxAmount, DiscountAmount: o.DiscountAmount,
		TotalAmount: o.TotalAmount, PaidAmount: o.PaidAmount, RemainingAmount: o.RemainingAmount,
		Notes: textPtr(o.Notes), CreatedAt: tsToPg(o.CreatedAt), UpdatedAt: tsToPg(o.UpdatedAt)}
}

func invoiceRow(inv *finance.Invoice) qdb.GetInvoiceByIDRow {
	return qdb.GetInvoiceByIDRow{ID: inv.ID, InvoiceNumber: inv.InvoiceNumber,
		SalesOrderID: pgtype.Int8{Int64: inv.SalesOrderID, Valid: true}, CustomerID: inv.CustomerID,
		InvoiceDate: inv.InvoiceDate, DueDate: inv.DueDate, Status: inv.Status, Subtotal: inv.Subtotal,
		TaxAmount: inv.TaxAmount, TotalAmount: inv.TotalAmount, PaidAmount: inv.PaidAmount,
		RemainingAmount: inv.RemainingAmount, Notes: textPtr(inv.Notes), CreatedAt: tsToPg(inv.CreatedAt),
		UpdatedAt: tsToPg(inv.UpdatedAt)}
}

func cashflowRow(c *finance.Cashflow) qdb.GetCashflowByIDRow {
	return qdb.GetCashflowByIDRow{
		ID: c.ID, TransactionNumber: c.TransactionNumber, Type: c.Type, Category: c.Category,
		TransactionDate: c.TransactionDate, Amount: c.Amount, Description: textPtr(c.Description),
		ReferenceType: textPtr(c.ReferenceType), ReferenceID: int8Ptr(c.ReferenceID),
		CreatedBy: int8Ptr(c.CreatedBy), StoreID: int8Ptr(c.StoreID),
	}
}

func paymentDetailRow(p *finance.Payment) qdb.GetPaymentByIDForUpdateRow {
	return qdb.GetPaymentByIDForUpdateRow{
		ID: p.ID, PaymentNumber: p.PaymentNumber, PayableType: p.PayableType, PayableID: p.PayableID,
		PaymentDate: p.PaymentDate, Amount: p.Amount, PaymentMethod: p.PaymentMethod,
		BankName: textPtr(p.BankName), BankAccount: textPtr(p.BankAccount),
		ReferenceNumber: textPtr(p.ReferenceNumber), Notes: textPtr(p.Notes),
		ProofUrl: textPtr(p.ProofURL), CreatedBy: int8Ptr(p.CreatedBy),
	}
}

func stringOrEmpty(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}
