Commit Graph
80 Commits
Author SHA1 Message Date
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 296708244e feat(enakgame): budget controller recommendations and analytics
EnakGame phase 9 of docs/tasks-enakgame.md (EG-901 to EG-903).

Budget Controller (EG-901, EG-902)
- GET /marketing/enakgame/budgets/:id/recommendation, GLOBAL budgets only: the
  multiplier (budget − realized) / (forecast − realized), within one step of 1,
  rounded down to two decimals, either way. Shows each game's new rules.
- POST .../recommendation/accept with the multiplier the admin saw: recomputed in the
  transaction, then one new ACTIVE version per game, the old one RETIRED, audited
  with source budget_controller and RECOMMENDATION_ACCEPTED on the budget.
- Migration 000112: base_config_id, multiplier and budget_id on
  game_reward_configs. Rules are always scaled from the admin's last version, so
  rounding does not compound and min/max are against what the admin set.
- Guardrails in game_budgets.thresholds: max_step_percent 10, min/max multiplier
  50-150%, cooldown_days 7 per organization. Provisional pending RFC §19.2 #4.
- RewardCalculator.Scale for the four reward types: amounts only, rounded down.

Analytics (EG-903)
- GET /marketing/enakgame/analytics/games and /analytics/economy over a range of
  Asia/Jakarta days (at most 366), from game_sessions and the wallet ledger.
- Migrations 000113 (game_sessions by organization and start) and 000114
  (wallet_transactions by organization and time, CONCURRENTLY).

The Postgres tests for accepting and analytics were not run: no test database here.
Migrations 000112-000114 have not been run anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:18:31 +07:00
efrilmandClaude Opus 5.5 798a36bd6c feat(enakgame): game sessions, rewards, vouchers, budgets and events
EnakGame phases 1-8 of docs/tasks-enakgame.md (EG-101 to EG-803), built on the
existing EnakPoint/EnakCoin wallet (docs/rfc-enakgame.md).

Foundation (phase 1)
- Migrations 000103-000106: games extended with organization, slug, status,
  entry cost and result rules, old games archived (not deleted); budgets,
  versioned reward configs, sessions and session rewards; the ledger types
  GAME_SPEND_REFUND, GAME_REWARD and REWARD_REDEEM_REFUND; audit_logs.
- AuditLogger writes in the caller's transaction only.
- enakgame.limit.user_daily and global_daily organization settings.

Games and sessions (phases 2-4)
- Admin /marketing/enakgame: games, reward config versions (immutable but for
  status, one ACTIVE per game), budgets with non-overlapping global periods and
  a daily job opening the next month.
- Customer /customer/enakgame: start (Idempotency-Key, entry cost and config
  frozen on the session), complete (result validation, reward engine, max_reward
  cap, daily limits via game_reward_counters, one GAME_REWARD per budget),
  automatic refunds for system errors and deactivated games, and a session job.
- Reward engine: FIXED, SCORE_BASED, OUTCOME_BASED, PROBABILITY (crypto/rand),
  rounded down.

Vouchers and budgets (phases 5-6)
- Migration 000108 and 000107: vouchers, codes, redemptions, cost attribution;
  Economy Guard counters.
- STATIC and CODE_POOL redemption in one transaction with the REDEEM PIN action;
  realized cost traced through the lots to the budget that paid the reward.
- Budget metrics: realized cost, forecast, exposure and status. Migrations
  000109-000110 add the wallet_lots indexes they need, built CONCURRENTLY.

Events (phase 7)
- Migration 000111: game events, each with its own EVENT budget. Event extras
  stack per PRD §16 defaults, with event and per-customer limits.

External vouchers (phase 8)
- VoucherProvider contract, two-step PENDING redemption and a recovery job,
  tested with a fake provider. No provider adapter is registered yet, so
  EXTERNAL vouchers stay out of the catalog.

Not yet decided before release: reward rounding, event stacking, budget
exhaustion policy and thresholds (RFC §19.2). Migrations 000103-000111 have
not been run on any shared database.

Also fixes a leftover PAYMENT filter in a wallet test and a data race in a
test PIN fake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:53:14 +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
efrilm b0ef226af1 feat: add printer types 2026-10-01 22:26:44 +07:00
efrilmandClaude Opus 5.5 c9654a387a fix(migrations): keep every payment method type in use
Production allows edc and delivery payment methods through a
payment_methods_type_check changed outside the migrations, and has rows of
both. 000094 rewrote the constraint with only cash, card, digital_wallet and
point, so it failed on production (in its transaction, leaving the database
dirty at 94 with nothing applied).

000094 now keeps edc and delivery, down included, and also allows qr, which
the code accepts but no constraint did. 000098 sets the same list where the
old 000094 already ran, as on staging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:00:14 +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 550122f29c feat(loyalty): remind customers before balances expire
Adds what the customer sees of expiry (docs/prd-point-coin.md F6, F12,
PC-504).

GET /customer/wallet/expiring lists everything that will expire, per
currency and day, soonest first. GET /customer/wallet already had the
nearest expiry per currency.

The expiry job now also sends reminders, with the settings of note N4 as
decided: once, reminder_days before (7 by default, 0 for none), per
currency. A customer gets one FCM push per currency and expiry day,
however many lots make it up: "150 EnakPoint akan kedaluwarsa pada 31 Okt
2026. Pakai sebelum hangus.", with type WALLET_EXPIRING, the currency,
amount and expiry_date in its data. Reminders cover whatever falls within
the window, so a run that was missed catches up rather than skipping a day.

Migration 000097 adds wallet_expiry_reminders, one row per customer,
currency and expiry day. The row is written before the push is sent, so
several instances of the job or a restart never remind twice; a push that
then fails is logged and not retried. Lots that expire later on the same
day as an earlier reminder are not reminded of again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:35:12 +07:00
efrilmandClaude Opus 5.5 bf9651e221 feat(loyalty): notify transfer recipients through FCM
The recipient of a transfer now gets a push through FCM instead of a
WhatsApp message (docs/prd-point-coin.md F5).

Customers had nowhere to keep FCM tokens: user_devices only holds staff
devices. Migration 000096 adds customer_devices, and the customer app
registers with PUT /customer/devices { device_id, fcm_token, platform,
app_version } after login and whenever FCM refreshes the token, and
unregisters with DELETE /customer/devices/:device_id on logout. A token
belongs to one customer only: registering it takes it away from whoever
had it on that phone before, so they stop getting this customer's
notifications.

The push goes to every device of the recipient after the commit, titled
"EnakPoint masuk" or "EnakCoin masuk", with type WALLET_TRANSFER_IN, the
TRANSFER_IN transaction id, the group id, the currency and the amount in
its data so the app can open it. A retried transfer sends nothing again. It
stays best effort: no device, FCM not configured or FCM failing is logged
and never undoes the transfer.

The app builds one FCM client and shares it between staff notifications
and customer pushes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:30:14 +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 8370851ed2 feat(loyalty): customer PIN
Adds the 6-digit customer PIN that approves every action moving EnakPoint
or EnakCoin on the customer's request (docs/prd-point-coin.md K8, F11, Q16,
Q17, PC-301).

Migration 000093 adds the PIN columns to customers and the
customer_security_events table. PIN data is read and written only through
CustomerPinRepository, never the Customer entity, so the hash cannot reach
a customer response. Only a bcrypt hash is stored.

- /customer/pin: status, OTP (pin_setup, pin_reset), create, change,
  reset. The OTP must be for that purpose and sent to the customer's own
  number; the existing OTP validation checks neither. A new PIN is checked
  (6 digits, confirmed, not one digit, not a run up or down, not the birth
  date as DDMMYY or YYMMDD) before the OTP is spent.
- Five wrong attempts in a row lock the PIN for 30 minutes; the counter is
  incremented in one statement so attempts at the same time all count,
  and a lock that ran out starts a new series. A locked PIN is refused even
  when right. The customer is told by WhatsApp, as there is no push channel
  to customers yet; only the attempt that reached the limit alerts.
- A reset through OTP lifts the lock and holds outgoing transfers for 24
  hours; paying and exchanging still work, and a held transfer costs no
  attempt.
- VerifyPin(ctx, customer, pin, action) for the flows that follow, with
  PIN_NOT_SET, PIN_INVALID (attempts left), PIN_LOCKED and
  TRANSFER_BLOCKED (until when), which PinErrorResponse turns into
  distinct codes and statuses.
- DELETE /marketing/customers/:id/pin (loyalty managers, reason required)
  and GET /marketing/customers/:id/security-events, scoped to the
  organization.

Every PIN event is in the security log with IP and user agent. No message
or binding error contains a PIN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:20:29 +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 b107f4ef04 feat(settings): add organization settings and loyalty setting history
Migration 000091 creates organization_settings, a key-value store per
organization shaped like outlet_settings, for the loyalty settings that must
be the same in every outlet (point value, exchange rate, transfer limits,
expiry). Until now there was nowhere to keep organization-level settings.

Also creates loyalty_setting_changes, the append-only log of who changed
which loyalty setting from what to what (PRD F2), for both organization and
outlet settings (PC-102).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:01:02 +07:00
efrilmandClaude Opus 5.5 84401cc708 feat(wallet): add wallet, ledger and lot tables
Migration 000090 creates customer_wallets, wallet_transactions, wallet_lots
and wallet_lot_allocations as specified in docs/prd-point-coin.md §8 (PC-101).

The CHECK constraints enforce K5 at the database: every ledger row names its
source or destination, PAYMENT and the other point-only types cannot carry
COIN, transfers need a counterparty, reversals need the row they reverse,
adjustments need an admin and a reason, and EXPIRE must point at a lot.
Balances and lot remainders cannot go negative, and a lot cannot hold more
than it was created with.

Beyond §8, adds idx_wallet_lot_allocations_lot_id: the primary key cannot
serve lookups by lot, which the reconciliation job needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:01:01 +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
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 e345aeee97 feat: new users role 2026-06-19 13:31:33 +07:00
Efril 503fb5734f fix: migration 82 2026-06-18 16:34:37 +07:00
ryan 87540fa1b7 Update exclusive summary 2026-06-18 15:44:15 +07:00
ryan 66d4c9f0af Update purchase order with outlet id 2026-06-18 15:27:20 +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 8c4d9c69d0 Update analytic to support new categories change 2026-06-15 14:17:48 +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 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
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 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 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 91960f0e57 categories add outlet id 2026-05-21 21:27:57 +07:00
ryan 4130cb66df refactor and add outlet product table 2026-05-13 21:58:54 +07:00
Efril 9f653eef37 fix migration number 2026-05-10 13:30:40 +07:00
Efril ddaf6df436 migration notification 2026-05-10 12:35:44 +07:00
ryan 2c34578a98 Merge remote-tracking branch 'origin/feature/notification' into self-order+notification
# Conflicts:
#	go.mod
#	go.sum
#	internal/app/app.go
#	internal/router/router.go
2026-05-10 12:23:16 +07:00
Efril bbd6666299 user devices 2026-05-10 10:42:09 +07:00