Commit Graph
56 Commits
Author SHA1 Message Date
efrilmandClaude Opus 5.5 19afa50b9c feat(customers): store customer phone numbers as 62…
A customer's phone number (customers.phone_number, the one they log in with) has
one stored form, 62 followed by the number without its 0: 6281234561234.
util.NormalizePhoneNumber rewrites 0812…, +62 812…, 62812…, 812… and +62 0812…,
with spaces, dashes, dots or parentheses, to it, and refuses anything that is not
an Indonesian mobile number (628 and 7 to 11 more digits).

It is applied wherever a customer types their number: check-phone, register,
login and resend-OTP (the validator rewrites the request), and the transfer
recipient. Before, the number was matched as typed and a leading 0 was refused,
so the same customer written another way was not found. The masked recipient
stays 08**-****-1234 as customers write numbers.

Migration 000116 rewrites the numbers already stored in customers and
otp_sessions. When several become the same number, the customer that already has
it keeps it, else a registered one, else the oldest; the others, and numbers that
are not Indonesian mobile numbers, are left as they are and cannot log in until
fixed by hand (the query to find them is in the migration). Down does nothing.

The migration was run, twice, against Postgres with a reduced customers and
otp_sessions schema and sample numbers; not against the real schema. The
Postgres tests were not run. customers.phone (the POS contact number) is not
touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:58:24 +07:00
efrilmandClaude Opus 5.5 18e87398bb feat(enakgame): spin as an EnakGame game; remove the old game flow
EnakGame phase 10 of docs/tasks-enakgame.md (EG-1001 to EG-1003).

Spin (EG-1001)
- PROBABILITY entries take an optional label (a wheel segment). The customer game
  list shows a PROBABILITY game's prizes (entry, label, amount, never weights), and
  completing returns the drawn prize, so the client can draw the wheel and stop it
  on the server's draw.
- docs/enakgame-spin.md: the admin steps to set up spin per organization (no
  seeder) and the customer app flow. An HTTP test plays it end to end.

Old game flow removed (EG-1002)
- Routes POST /customer/spin, GET /customer/games, GET /customer/ferris-wheel, and
  admin /marketing/games, /marketing/game-prizes, /marketing/rewards, with their
  handlers, services, processors, repositories, validators, models, contracts,
  mappers and tests (GamePlayProcessor, SpinGameService, rewards, ...). This also
  closes RFC §15 findings 1 and 2 (double charge, spinning another org's game).
- Tables games, game_prizes, game_plays and rewards stay for ledger history.
  entities.StringSlice moves to its own file; the omset tracker (unrouted) keeps
  game_id but no longer embeds the old game response.

games.is_active dropped (EG-1003)
- Migration 000115; nothing reads metadata.coin_cost any more.

The EnakPoint integration docs now point at /customer/enakgame. The Postgres tests
were not run: no test database here. Migration 000115 has not been run anywhere.
The customer app must stop calling the removed endpoints before this is deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:31:56 +07:00
efrilmandClaude Opus 5.5 2c9753fae7 feat(loyalty): remove paying with EnakPoint
EnakPoint can only be redeemed for vouchers now: it can no longer pay for
orders and is never cashed out (docs/enakgame-prd.md §3.2, EG-001,
EG-002). No order was ever paid with EnakPoint, so there is no data to
move.

Removed:
- POST /customer/wallet/payment-code, POST /customer/orders/:id/pay-with-points
  and GET /orders/:id/point-payment/preview, with their processors,
  repositories, services, handlers and tests.
- The point payment method type: paying, splitting and refunding with it,
  the outlet filter on the method list, and the system-method guard.
- points and payment_code on CreatePayment; points_used and point_value
  on payments; accepts_point_payment on the customer outlets.
- The outlet point_payment settings. A PUT that still sends them is
  rejected as an unknown field.
- The EnakPoint split in the payment method analytics.
- PAYMENT and PAYMENT_REFUND from the wallet type rules. Tests that used
  them as a generic EnakPoint debit use REWARD_REDEEM.
- The EnakPoint-paid part from the earning basis, which is
  subtotal − discount again.

Migration 000102 drops the trigger, the point methods and their index,
the payments columns, and the outlet settings, and restores the method
type CHECK without point. payments.payment_method_id is ON DELETE
RESTRICT, so it fails rather than lose a payment made with EnakPoint.

The integration docs list the removed endpoints and fields, and the
EnakPoint & EnakCoin PRD and tasks note what is superseded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:48:29 +07:00
efrilm 117e3529aa feat: update profit sharing 2026-10-04 21:54:13 +07:00
efrilm fbe7e97dc7 feat: add limit owner 2026-10-02 23:30:03 +07:00
aefril d3e3ea6b08 Merge pull request 'feat: add printer types' (#40) from fix/point-payment-method-types into main
Reviewed-on: #40
2026-10-01 17:43:53 +02:00
efrilm b0ef226af1 feat: add printer types 2026-10-01 22:26:44 +07:00
efrilmandClaude Opus 5.5 8bf2fe5585 fix(customer-auth): make organization_id optional at registration
Requiring organization_id broke the current app, which does not send it.
When it is left out and the database has exactly one organization, the
customer now joins that one, so the app works unchanged. A sent
organization_id must still exist, and with several organizations and none
sent registration is refused with a clear message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:16:31 +07:00
efrilmandClaude Opus 5.5 a3cb7dd5fd fix(customer-auth): register customers into the app's organization
Registration put every new customer into a hardcoded organization id,
which does not exist in staging, so set-password failed on the
fk_customers_organization foreign key with a 500.

POST /customer-auth/register/start now takes organization_id, the
organization the app is built for. It must be a UUID of an existing
organization, checked before the OTP is sent; it is kept in the OTP
session and set-password creates the customer there. A registration
started before this change has no organization in its session and is asked
to start again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:12:26 +07:00
efrilmandClaude Opus 5.5 c988a79d3b refactor(loyalty): remove what is left of tokens
Tokens are EnakCoin and no app uses the token names any more, so their
compatibility layer goes:

- GET /customer/tokens and its handler, service, processor and response
  types.
- total_tokens and tokens_history on GET /customer/wallet; last_updated
  now comes from the most recent row of either currency.
- token_used and tokens_remaining on game and spin responses, and
  sort_by=token_used on the game play list.
- TOKENS as a campaign type and reward type, with the mapping to COINS:
  migration 000092 already renamed the stored values.

The customer_tokens table and its entity stay, as cmd/wallet-migrate still
reads them, and LEGACY_TOKENS stays as the reference of the MIGRATION rows
it wrote. The docs list the removed names and their replacements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:55:27 +07:00
efrilm 582dc75543 Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts commit 4e24f9bbb0.
2026-09-30 15:31:44 +07:00
efrilmandClaude Opus 5.5 4e24f9bbb0 Revert "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts merge commit 645da30, returning main to f0ff59f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:16:15 +07:00
efrilmandClaude Opus 5.5 d1e543a79f refactor(loyalty): remove the dead customer points and tokens code
First part of PC-601 (docs/prd-point-coin.md §10.7): the code that has
had no way in since balances moved to the wallet.

- The /marketing/customer-points and /marketing/customer-tokens routes
  were commented out; their 16 handler methods, the GamificationService
  methods behind them, and the validators, transformers, mappers and
  contract/model types only they used are gone.
- CustomerPointsProcessor loses its "not implemented" stubs; it keeps the
  customer app's balance, wallet and games endpoints.
- CustomerTokensProcessor and the customer points and tokens repositories,
  wired but no longer called by anything, are gone.

What stays until its preconditions are met: the customer_points and
customer_tokens tables and their entities, which cmd/wallet-migrate still
reads, and the /customer/points, /customer/tokens aliases and the
token_used / tokens_remaining fields, until the apps no longer use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:43:15 +07:00
efrilmandClaude Opus 5.5 a18bb072f5 feat(loyalty): pay every game with EnakCoin
Games now spend the wallet's EnakCoin instead of the per-type tokens
(docs/prd-point-coin.md F8, K1, PC-403).

GamePlayProcessor.PlayGame charges the game's metadata.coin_cost, 1 when
it is not set; a cost that is not a whole number of at least 1 refuses the
game. In one transaction it picks the prize, takes the EnakCoin with a
GAME_SPEND row pointing at the new game_plays.id (which locks the wallet,
so a customer's plays at the same time queue up), records the play and
takes the prize from stock. The play owns its transaction, so the spin
service no longer wraps it, and the admin play endpoint is now atomic too.

The game, game prize and game play repositories go through DBFromContext
so they join that transaction. DecreaseStock now reports a prize that ran
out (ErrGamePrizeOutOfStock) instead of silently updating nothing; that,
or any other stock failure, cancels the whole play, where it used to be
only printed. The manual AddTokens rollback is gone. Not enough EnakCoin,
an inactive game or a prize that ran out answer 400 on /customer/spin
instead of 500.

game_plays.token_used is renamed coins_used (migration 000095). What a
play costs is no longer the caller's choice, so PlayGameRequest loses
token_used. Responses carry coins_used and coins_remaining; token_used and
tokens_remaining stay as deprecated copies until the apps move over, and
sort_by=token_used still sorts by coins_used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:20:42 +07:00
efrilmandClaude Opus 5.5 cf5332c281 feat(loyalty): EnakPoint payment method
Adds the system payment method for paying with EnakPoint
(docs/prd-point-coin.md F9, §8, §10.5, PC-303).

Migration 000094 allows the point type, keeps one per organization with a
partial unique index, creates it for every existing organization, and adds
a trigger that creates it for new ones, as the walk-in customer is. It adds
payments.points_used and point_value. Their CHECK is written so it can
never be NULL: the PRD form, (both NULL) OR (both > 0), is NULL for
points_used with a NULL point_value, which a CHECK lets through, so a
payment could have lost the value a refund depends on. A test caught it.

The API cannot create, delete or retype the EnakPoint method, nor turn
another method into one; that answers 400. Renaming it is allowed. The
method list takes the outlet from ?outlet_id= or the user's outlet and
leaves EnakPoint out when that outlet does not accept it, filtered in the
query so the count stays right. The organization-wide active list is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:33:25 +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 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 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
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 a7c2d6cbb3 feat(category): update list filter category 2026-08-06 21:21:41 +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 c1d859ebdd Make vendor nullable 2026-06-18 11:04:59 +07:00
ryan 2921631ac3 Revert "Revert purchase order"
This reverts commit 657a201fc0.
2026-06-17 18:31:10 +07:00
ryan 6c19876a47 Fix issue 2026-06-15 17:44:25 +07:00
ryan 657a201fc0 Revert purchase order 2026-06-15 13:52:08 +07:00
ryan 1718c5adab Fix expense to be nullable without raw material. 2026-06-10 14:25:23 +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 29aeb58fc0 Fix formatting 2026-06-08 12:30:39 +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 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
ryan a55a3f4ee2 add expense_name 2026-05-26 15:25:47 +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
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
Efril 9d71b339b5 notification 2026-05-10 10:57:38 +07:00
Efril bbd6666299 user devices 2026-05-10 10:42:09 +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 12ee54390f Revert "voucher"
This reverts commit cfe690a40f.
2025-09-16 19:31:39 +07:00
Aditya Siregar cfe690a40f voucher 2025-09-13 15:37:26 +07:00
Aditya Siregar 4f6208e479 fix 2025-09-13 02:17:51 +07:00