Skip to content

feat(loyalty): global earning rate overhaul - #169

Merged
itsbkm merged 9 commits into
mainfrom
feature/loyalty-global-rate
Aug 2, 2026
Merged

feat(loyalty): global earning rate overhaul#169
itsbkm merged 9 commits into
mainfrom
feature/loyalty-global-rate

Conversation

@khaira777

Copy link
Copy Markdown
Contributor

Implement global loyalty earning with tri-state product overrides.

Summary

  • NULL: inherit global earning rate
  • 0: no earning
  • >0–100: custom earning rate

Key Changes

  • Add a non-destructive database migration that seeds settings.global_cashback_percent to 0.
  • Extend GET/PUT /api/settings/loyalty to return and persist global_cashback_percent.
  • Update product create/update APIs to preserve explicit null, 0, and positive values.
  • Updated CSV importer to map blank fields to NULL, and validate proper numbers.
  • Added strict type checking for numbers (0-100 range) on all endpoints avoiding Boolean/null coercion.
  • Update billing payment logic to fallback to the global setting when product returns NULL.
  • Re-wrote full integration tests ensuring the new tri-state behavior works perfectly.

Closes #81

Implement global loyalty earning with tri-state product overrides (NULL inherits, 0 disabled, >0 custom).
- Added global_cashback_percent setting to DB (v39 migration).
- Validated loyalty rates on settings, products APIs and CSV uploads to strictly enforce 0-100 range.
- Resolved trailing whitespace and type conversion gaps (Number vs parseFloat vs Boolean).
- Tests cover full inheritance path and negative payloads.

Closes #81
@itsbkm
itsbkm force-pushed the feature/loyalty-global-rate branch from 339e104 to 8cea66e Compare July 30, 2026 16:13
itsbkm and others added 3 commits July 30, 2026 21:44
…rsion

Rebasing this branch onto main surfaced a real collision: this PR's
migration and an unrelated main-branch migration (#173's
add_users_tokens_valid_after) both claimed version 39. Renumbered this
one to 40 to fix that, which broke this test's hardcoded
`m.version === 39` lookup — switched to finding it by name instead so
a future renumber can't silently break this assertion again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…it opt-in

Three fixes on top of the global earning rate work so it can merge and so it
does not change what an upgraded store pays out.

Migration numbering. main gained migrations 40 (v2_cloud_defaults_and_tax_toggle)
and 41 (support_ticket_outbox) while this branch was open, and this one was also
40. runMigrations() derives its target from the last array entry and throws when
current > target, so a duplicate 40 either bricks startup on every upgraded
install or is silently skipped and never seeds the setting. Renumbered to 42.

Preserve existing cb_percent. Dropped
  UPDATE products SET cb_percent = NULL WHERE cb_percent = 0
Under the tri-state, 0 means "earns nothing" and NULL means "inherit the global
rate". The old schema default was 0, so that statement converted essentially
every product in every existing install into an inherit. It looks harmless only
because the global rate seeds to 0 - the damage lands later, when an owner sets
a rate and every product they had deliberately excluded silently starts paying
out. "Never configured" and "deliberately zero" are indistinguishable in the old
data, which is exactly why the migration must not guess.

Give the owner the choice instead. GET /api/products/loyalty/global-rate-candidates
counts products currently earning nothing, and POST .../apply-global-rate
switches them to inherit. Settings > Loyalty surfaces the count and the action
only when there is something to apply. Without this the feature is inert on
every upgraded install: the owner sets a rate and nothing happens, because no
product is NULL.

Also reverted the schema-health suppression for the cb_percent default. Rather
than silencing the detector, createSchema() keeps DEFAULT 0 so a fresh install
and an upgraded one have an identical products table - SQLite cannot alter a
column default without rebuilding the table. The tri-state does not depend on
the default; every insert path passes cb_percent explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@itsbkm
itsbkm merged commit 9aee266 into main Aug 2, 2026
2 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: Refine Loyalty Program Onboarding and Labeling

2 participants