Commit Graph
148 Commits
Author SHA1 Message Date
efrilmandClaude Opus 5.5 e5db0325cc feat(wallet): serve customer balances and history from the wallet
GET /customer/wallet now reads the EnakPoint & EnakCoin wallet
(docs/prd-point-coin.md F6, PC-106): spendable point and coin balances, the
rupiah value of one EnakPoint and of the balance, the nearest day each
currency loses balance (grouped by Asia/Jakarta day), and recent ledger
rows. The fields of the pre-wallet response stay, filled from the wallet, so
app versions that read them keep working.

Adds GET /customer/wallet/transactions with pagination and filters for
currency, one or more types, and an inclusive date range. Each row shows
where the value came from (additions) or went to (deductions) as in §8.1,
and additions list their lots and earliest expiry. The counterparty id, the
admin and the metadata are left out; the description already carries the
masked name. A malformed query answers 400, a missing customer 404.

/customer/points and /customer/tokens keep their shape and now read the
wallet too, so customer_points_repository is no longer used for balances.

Balances are what the customer can spend: lots that have expired but that
the expiry job has not processed are not counted. The point value is read
from organization_settings (loyalty.point.value, default 1) through a small
repository that the typed settings reader in PC-109 will build on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:15:26 +07:00
efrilmandClaude Opus 5.5 41b75810fd feat(wallet): migrate legacy points and tokens into the wallet
Adds cmd/wallet-migrate (make wallet-migrate, args=-dry-run to only report),
which moves customer_points and customer_tokens into the wallet
(docs/prd-point-coin.md §10, PC-105). Each customer gets a MIGRATION ledger
row and a non-expiring lot per currency, written through WalletProcessor in
one transaction per customer. EnakCoin is the sum of every token type (Q6),
with the legacy rows listed in the row's metadata.

It credits the difference between the legacy balance and what earlier runs
migrated, so running it again never doubles a balance and picks up only
what the old code added since. A legacy balance that shrank after being
migrated is reported and left alone, since only an admin adjustment may
take balance away, and the command then exits non-zero. It ends with a
legacy / migrated / wallet total per currency.

Migration 000092 renames TOKENS to COINS in campaigns.type and
campaign_rules.reward_type. The campaign API now validates COINS; it still
accepts TOKENS, including as a list filter, and stores it as COINS so older
dashboards keep working while they are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:47:48 +07:00
efrilmandClaude Opus 5.5 2eb590caab feat(wallet): add wallet engine as the only way to change a balance
WalletProcessor writes the balance, the ledger row and the lots or
allocations together, which keeps SUM(ledger) = balance = SUM(lot
remaining) (docs/prd-point-coin.md §7.5, PC-104).

- Credit writes the ledger row and creates lots, each with its own expiry
  and origin lot.
- Debit draws from the preferred lots first (a reversal's own lots, or the
  lot being expired), then from unexpired lots in K9 order, and returns the
  allocations with their expiry so CarryOver can give the receiving side of
  a transfer or exchange the same expiry.
- DebitUpTo takes what the wallet has and reports the shortfall (F10, Q3).
- An idempotency key returns the first result; reusing it for a different
  operation is an error.
- §8.1 is checked in code from one rule table, ahead of the database
  constraints, so callers get a readable error.

Each method locks the wallet itself, after validating the input and before
checking the idempotency key, so correctness does not depend on the caller.
Operations on two wallets still call LockWallets first to keep lock order.

Unit tests run on an in-memory repository and check the §7.5 invariants
after every scenario; one more test runs the engine against Postgres when
TEST_DATABASE_URL is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:47:37 +07:00
efrilmandClaude Opus 5 ebf666c004 feat(product): require a unit for weight-based products
A product sold by weight with no unit produces order lines with nothing to
print: the receipt would read "4,2" with no idea of what. Until now nothing
stopped that — the mistake only surfaced at the cashier.

Enforce it in two places, because neither alone sees the whole picture. On
create, the validator has everything it needs. On update, the request may
omit unit_id for a product that already has one, so the check runs in the
processor against the merged product: what is rejected is the end state, a
product sold by weight with no unit.

Also fixes two things this uncovered:

The struct tags on the product contracts are decorative — this validator is
hand-written and never calls validator.Struct — so `oneof=unit weight` was
never enforced, and an unknown sell_by was silently rewritten to "unit" by
the mapper. It is now rejected with a message that names the valid values.

The update validator's "at least one field" guard did not list unit_id,
sell_by or print_to_checker, so an update carrying only one of those was
turned away as an empty request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:29:34 +07:00
efrilmandClaude Opus 5 992bb04816 feat(order): support weight-based products
Products like fish are sold per weighing (4.2 ons, 5.6 ons), which the
order line could not represent: quantity is INTEGER and prices are always
computed as quantity * unit_price.

Model one weighing as one order line. quantity stays INTEGER and keeps
meaning "how many items"; the measured amount goes into a new nullable
order_items.weight, and the line is priced weight * unit_price. Two
weighings of the same product are two lines, never merged into one.

Keeping quantity integral avoids float comparisons in void, refund and
split bill, where accumulated rounding error would silently misbehave —
"1.4 + 1.4 + 1.4" is not 4.2 in float64, which would leave a fully paid
split-bill item marked unpaid.

BillableQuantity() is now the single place that decides between weight
and count; every price and cost calculation goes through it. Missing one
would bill a 4.2 ons fish as a single ons — wrong money, no error.

Two database constraints back the design: a weighed line always carries a
positive weight, and its quantity is pinned to 1. The latter also makes
void all-or-nothing for weighed lines, so the row-splitting branch can
never produce a zero-weight remainder row.

Also wires product.unit_id through the API, which was previously not
settable at all, and corrects the misleading comment on the request's
unit_price field — that value has never been used; price always comes
from the database.

Design notes and the audit of every price multiplication site are in
docs/rfc-weight-based-products.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:14:42 +07:00
Efril 2c6864147b feat: cash advance 2026-08-13 14:38:28 +07:00
Efril e6078e3c0b feat(purchase 2026-08-11 22:41:10 +07:00
efrilm dbc143954c feat(purchase): unwired unit convertions 2026-08-11 21:24:24 +07:00
efrilm 0726fcecf0 feat(ingredients): make units nullable 2026-08-11 21:20:17 +07:00
efrilm 9ae5be2c33 feat(purchasae): added team with category parent and central 2026-08-11 21:00:39 +07:00
Efril b9ac97178f feat: profit sharing 2026-08-05 19:28:38 +07:00
Efril 9b0fc9a63b feat: updat analytic profit loss add purchasing 2026-06-24 00:11:04 +07:00
Efril 793919cf10 feat: add outlet name at analytic response and new overview dashboard 2026-06-23 22:18:16 +07:00
ryan 66d4c9f0af Update purchase order with outlet id 2026-06-18 15:27:20 +07:00
ryan 55119b3e91 Add MTD 2026-06-18 14:19:45 +07:00
ryan c1d859ebdd Make vendor nullable 2026-06-18 11:04:59 +07:00
ryan 2ad9e2f85f Fix total_amount to use base price 2026-06-18 09:52:16 +07:00
ryan 0db838e2c4 Fix bank 2026-06-17 18:26:40 +07:00
ryan 4b6cbb69c1 Add exclusive-summary 2026-06-17 18:17:08 +07:00
ryan 1718c5adab Fix expense to be nullable without raw material. 2026-06-10 14:25:23 +07:00
ryan d0c090a657 Update purchase analytics 2026-06-10 13:18:49 +07:00
ryan c3db919531 Update expense for product category (non-inventory type) 2026-06-10 12:42:53 +07:00
ryan e09feff36d Update purchase for product category (inventory type) 2026-06-09 15:59:34 +07:00
ryan e7dd9660da Merge remote-tracking branch 'origin' into feature/expense
# Conflicts:
#	internal/router/router.go
2026-06-09 13:25:09 +07:00
Efril c57620beeb feat: update product analytic 2026-06-08 19:32:30 +07:00
ryan 69d8c8ce5e Add category table 2026-06-08 12:29:59 +07:00
Efril ea9dceb333 fix: prevent race condition on order subtotal calculation 2026-06-03 23:59:15 +07:00
ryan 094e8b2a47 Add expense analytics 2026-06-03 14:56:27 +07:00
ryan b90a3cde4a Fix coa summary zero 2026-06-03 13:00:50 +07:00
ryan 7c8c7fb7db update expense coa 2026-06-02 16:32:20 +07:00
ryan 47fa21d739 reinstate profit loss overview 2026-06-01 13:13:40 +07:00
ryan dc13bb5f93 update due date and range date 2026-05-29 18:24:14 +07:00
ryan d26f5c5354 add status to expense 2026-05-29 15:44:59 +07:00
ryan 1b7bec4f81 update expense item name 2026-05-29 13:25:38 +07:00
Efril f7399fd0e7 Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-pos-backend into feature/expense 2026-05-29 12:34:34 +07:00
Efril 84222fc7f4 update 2026-05-28 15:30:18 +07:00
Efril 23ac572e3f add print_to_checker at product outlet 2026-05-28 13:49:57 +07:00
ryan a55a3f4ee2 add expense_name 2026-05-26 15:25:47 +07:00
ryan 024d9ee637 Update profit-loss 2026-05-26 14:59:56 +07:00
Efril 957c1ae53d update order response 2026-05-25 20:28:24 +07:00
ryan b8be29e110 Add item_expense 2026-05-25 16:19:36 +07:00
ryan da87d659df Add expense CRUD 2026-05-25 14:59:40 +07:00
Efril d0378b5ac4 update category 2026-05-21 23:05:25 +07:00
Efril 72f67cb519 create or update product assign to product outlet 2026-05-21 21:20:54 +07:00
ryan 35e7152abb add purchasing in analytics endpoint 2026-05-19 14:45:26 +07:00
Efril 50d633ee3a fix products 2026-05-14 01:19:45 +07:00
Efril 21fa21d089 get products all 2026-05-14 00:15:28 +07:00
ryan 5f379faf17 change product list to retrieve its data from product outlets 2026-05-13 23:15:09 +07:00
ryan 3b62504798 fix 2026-05-13 22:30:55 +07:00
ryan 4130cb66df refactor and add outlet product table 2026-05-13 21:58:54 +07:00