Commit Graph
37 Commits
Author SHA1 Message Date
efrilmandClaude Opus 5.5 39e47ff0e6 feat(loyalty): typed loyalty settings with change history
Adds LoyaltySettingsProcessor (docs/prd-point-coin.md F1, F2, F12, PC-109).

Reading returns typed settings for an outlet (earning per currency, paying
with EnakPoint) and for an organization (point value, exchange rate,
transfers, and the expiry settings awaiting note N4). A key that was never
set takes the PRD default. A stored value that is unusable, such as an
earn_per_amount of 0 that would divide by zero, also falls back to the
default and is logged, so a bad row never reaches a calculation.

Writing takes the whole settings struct, validates every rule in the PRD
before touching the database, and stores and records in
loyalty_setting_changes only the keys whose effective value changes: old
value (NULL while it was on its default), new value, and who changed it.
Clearing a limit deletes the stored value. Each save runs in one
transaction under an advisory lock per outlet or organization, so two saves
at once cannot both compute their change from the same old value. The
outlet must belong to the caller's organization.

Every key is described once (key, default, valid range, bound field), and
reading, validating and diffing all use that description.

GET /customer/wallet now reads the point value through this processor; the
minimal organization settings repository from PC-106 is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:23:08 +07:00
efrilmandClaude Opus 5.5 a6d5a8b056 feat(wallet): customer wallet and manual adjustments in the dashboard
Adds the dashboard side of a customer's wallet (docs/prd-point-coin.md F7,
PC-107), under /marketing for admins and managers:

- GET /marketing/customers/:id/wallet returns the customer, the ledger and
  spendable balances, every lot that still holds something (flagged when
  expired), and a page of history. Unlike the customer's own view, each row
  carries the real names behind it: the transfer counterparty, the admin or
  cashier, and the outlet, plus the reason and metadata.
- POST /marketing/customers/:id/wallet/adjust takes a signed amount and a
  required reason. It writes an ADJUSTMENT pointing at the admin through the
  wallet engine, refuses to take more than the customer can spend, and
  accepts an optional idempotency key so a retried request adjusts once.
  Reasons describing a cash-out are refused (K7).

The customer must belong to the caller's organization; otherwise both
endpoints answer 404. Positive adjustments create non-expiring lots until
the expiry model is decided (F12, note N4).

The mapping from ledger rows to what the apps show is now shared between the
customer and dashboard views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:36:18 +07:00
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 fc5eecb68a feat(wallet): add wallet entities and repository
Entities for the four wallet tables and a WalletRepository that the wallet
processor will build on (PC-103).

Every method goes through the caller's transaction, and writes and locks
refuse to run without one: outside a transaction a lock is released as soon
as it is taken and a balance could move without its ledger row.

- LockWallet creates the wallet on first use, taking the organization from
  the customer, then locks it with SELECT ... FOR UPDATE.
- LockWallets always locks in customer_id order so opposite transfers
  cannot deadlock.
- AddBalance and ConsumeLot are conditional updates that return an error
  when they would overdraw, instead of tripping the CHECK constraint.
- ListActiveLots returns unexpired lots with balance in K9 spending order.

The tests need a real Postgres and run only when TEST_DATABASE_URL points at
a migrated database. Both the lock and the lock ordering were checked by
removing them and watching the tests fail (lost update, deadlock detected).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:01:03 +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 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 e345aeee97 feat: new users role 2026-06-19 13:31:33 +07:00
ryan 69d8c8ce5e Add category table 2026-06-08 12:29:59 +07:00
ryan dc13bb5f93 update due date and range date 2026-05-29 18:24:14 +07:00
ryan da87d659df Add expense CRUD 2026-05-25 14:59:40 +07:00
ryan 4130cb66df refactor and add outlet product table 2026-05-13 21:58:54 +07:00
ryan d38a770ec5 Add omset milestone scheduler with owner role and revenue tracking 2026-05-13 09:48:17 +07:00
Efril 9d71b339b5 notification 2026-05-10 10:57:38 +07:00
Efril bbd6666299 user devices 2026-05-10 10:42:09 +07:00
Efril a7022dd4c1 ingredient composition 2026-04-27 21:17:12 +07:00
Aditya Siregar a520d0ed11 fix campaign rules 2025-09-18 13:39:37 +07:00
Aditya Siregar 65f61b65cf user auth register 2025-09-18 01:32:01 +07:00
Aditya Siregar 155016dec8 Add Campaign 2025-09-17 23:55:11 +07:00
Aditya Siregar 201e24041b Add Tiers and Game Prize 2025-09-17 19:30:17 +07:00
Aditya Siregar efe09c21e4 Add coa purchase and vendors 2025-09-12 01:12:11 +07:00
Aditya Siregar 8e9c14b860 Add order items 2025-08-08 22:33:08 +07:00
Aditya Siregar 96743cf50b Update 2025-08-03 00:34:25 +07:00
Aditya Siregar a759e0f57c init 2025-07-30 23:18:20 +07:00
aditya.siregar 4f5950543e init 2025-07-18 20:10:29 +07:00
aditya.siregar 1201b2e45b Add void print 2025-06-24 02:47:44 +07:00
aditya.siregar 09c9a4d59d update 2025-04-10 11:21:08 +07:00
aditya.siregar c41826bb1b Update Member 2025-03-15 15:51:18 +08:00
aditya.siregar 18003313dd Update template email 2025-03-08 00:35:23 +07:00
aditya.siregar 764ea68bf9 Update Payment VA 2024-10-15 11:52:34 +07:00
fernanda-one b1c1c0260b feat: filter order history 2024-08-10 23:21:01 +07:00
aditya.siregar bdf930445c Customer Order 2024-08-04 01:16:25 +07:00
aditya.siregar 4ae9079ff9 add order and payment 2024-06-04 02:59:31 +07:00
aditya.siregar 8a23e72230 Add User Partner and Product 2024-06-03 14:40:50 +07:00
aditya.siregar 67f1dbc850 init project 2024-05-28 14:14:55 +07:00