Files
cfw-autumn/shared/models/cusModels
John Yeo 029e03daae feat: added spend limits to customer billing controls (#930)
## Summary
<!-- Provide a short summary of your changes and the motivation behind them. -->

## Related Issues
<!-- List any related issues, e.g. Fixes #123 or Closes #456 -->

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Refactor
- [ ] Other (please describe):

## Checklist
- [ ] I have read the [CONTRIBUTING.md](https://github.com/useautumn/autumn/blob/staging/.github/CONTRIBUTING.md)
- [ ] My code follows the code style of this project
- [ ] I have added tests where applicable
- [ ] I have tested my changes locally
- [ ] I have linked relevant issues
- [ ] I have added screenshots for UI changes (if applicable)

## Screenshots (if applicable)
<!-- Add before/after screenshots or GIFs here -->

## Additional Context
<!-- Add any other context or information about the PR here --> 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds customer-level spend limits with overage caps and an enable/disable toggle, enforced across check/track (including credit conversions) with Redis cache parity and stale-cache deletion on entity update errors. Also exposes `entities.update` in OpenAPI v2.1 and propagates `spend_limits` through DB, cache, and init/update flows.

- **New Features**
  - Support `billing_controls.spend_limits` on customers; enforced for direct customer, inherited entity-product/per-entity balances, and credit conversions.
  - Reject duplicate `feature_id` entries for spend limits on customers and entities.
  - Atomic Redis updates for `spend_limits`; prepend `LUA_UTILS` to the customer update Lua script with `is_nil` checks.
  - Expose `entities.update` in OpenAPI v2.1 with docs and examples.

- **Migration**
  - Rename types: `ApiEntityBillingControlsInput` → `ApiEntityBillingControlsParams`, `CustomerBillingControlsInput` → `CustomerBillingControlsParams`.
  - Update via `customers.update(customerId, { billing_controls: { spend_limits: [...] } })`; set `enabled: false` to disable a limit.
  - Update entity billing controls with `entities.update` using `billing_controls`.

<sup>Written for commit 1f61ea39fe32b04b81ac27bebc8cd5cf29bc0723. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- greptile_comment -->

<details><summary><h3>Greptile Summary</h3></summary>

This PR extends the **customer billing controls** feature to support **spend limits** at the customer level, mirroring the existing entity-level spend limit functionality. The `spend_limits` field is wired through the full stack: DB schema (`cusTable`), customer init, update handler, Redis cache Lua script, API contracts (`CustomerBillingControlsParamsSchema`), and OpenAPI spec. A notable side-fix is that `LUA_UTILS` (which defines the `is_nil` helper) is now correctly prepended to `UPDATE_CUSTOMER_DATA_SCRIPT`, enabling the Lua refactor from `~= nil and ~= cjson.null` to the cleaner `is_nil()` calls. Comprehensive integration tests are added covering check, track, and CRUD scenarios across all balance types.

**Key changes:**

- **Improvements** — `spend_limits` column added to the `customers` DB table and propagated through `initCustomer`, `updateCustomer`, `updateCachedCustomerData`, and the Redis Lua cache script.
- **Improvements** — `LUA_UTILS` is now included in `UPDATE_CUSTOMER_DATA_SCRIPT`, fixing latent missing-helper issue and enabling `is_nil()` refactor across all scalar-field nil checks in `updateCustomerData.lua`.
- **API changes** — `CustomerBillingControlsParamsSchema` introduced with duplicate-`feature_id` validation for `spend_limits`; `...Input` type/schema names renamed to `...Params` for consistency across entity and customer billing control types.
- **API changes** — `updateEntityContract` added to the OpenAPI v2.1 spec, exposing `POST /v1/entities.update` publicly.
- **Improvements** — `expectBoundaryAndParity` test utility updated to make `entityId` optional, allowing reuse in customer-level (no-entity) check tests.
- **Bug fixes** — `console.log("Customer data:", customerData)` debug statement left in `setupCreateCustomer.ts` should be removed before merge (logs potentially sensitive PII).
</details>

<h3>Confidence Score: 4/5</h3>

- Safe to merge after removing the debug console.log in setupCreateCustomer.ts.
- The feature is well-structured and closely mirrors the existing entity spend-limit and auto-topup patterns. Integration tests cover all major spend-limit scenarios (check, track, CRUD, enabled/disabled, entity-product, per-entity, credit-system). The only blocker is a leftover debug statement that logs raw customer data in production. All schema validation, cache, and DB wiring look correct.
- server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomer.ts — debug console.log must be removed.

<h3>Important Files Changed</h3>

| Filename | Overview |
|----------|----------|
| server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomer.ts | Debug `console.log("Customer data:", customerData)` was accidentally left in — logs potentially sensitive PII in production. |
| server/src/internal/customers/actions/update/updateCustomer.ts | Adds `spend_limits` propagation alongside `auto_topups` when billing controls are updated; relies on Drizzle/JSON.stringify omitting undefined for partial updates. |
| server/src/_luaScriptsV2/customers/updateCustomerData.lua | Adds `spend_limits` cache-update block mirroring the existing `auto_topups` pattern; also refactors scalar-field nil checks to use the `is_nil()` helper from LUA_UTILS. |
| server/src/_luaScriptsV2/luaScriptsV2.ts | Correctly prepends `LUA_UTILS` to `UPDATE_CUSTOMER_DATA_SCRIPT` so the `is_nil` helper is available in the Lua script that now uses it. |
| shared/models/cusModels/billingControls/customerBillingControls.ts | Introduces `CustomerBillingControlsParamsSchema` with duplicate `feature_id` validation for `spend_limits`; renames `...Input` type to `...Params` for consistency. |
| shared/models/cusModels/cusTable.ts | Adds a `spend_limits` JSONB column typed as `DbSpendLimit[]` to the `customers` Drizzle table definition. |
| shared/api/billingControls/entityBillingControls.ts | Renames `ApiEntityBillingControlsInputSchema` → `ApiEntityBillingControlsParamsSchema` and migrates duplicate-feature-id validation from `.superRefine()` to Zod v4's `.check()` API. |
| packages/openapi/v2.1/contracts/entitiesContract.ts | Adds `updateEntityContract` exposing the `POST /v1/entities.update` endpoint in the OpenAPI spec. |
| server/tests/integration/crud/customers/customer-billing-controls.test.ts | New integration tests covering customer spend-limit CRUD: create with spend limits, update without clearing other controls, and duplicate-feature-id rejection. |
| server/tests/integration/balances/check/spend-limit/check-customer-spend-limit.test.ts | New integration tests verifying that customer-level spend limits are respected on check calls across all balance types (lifetime, prepaid, consumable, entity-product, per-entity) and that disabled limits lift the cap. |
| server/tests/integration/balances/track/spend-limit/track-customer-spend-limit.test.ts | New integration tests verifying that customer-level spend limits cap overage on track calls, including credit-system conversion and disabled-limit bypass. |

</details>

<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant Client
    participant API as API (updateCustomer / createCustomer)
    participant DB as PostgreSQL (customers)
    participant Cache as Redis (FullCustomer)
    participant Lua as Lua (updateCustomerData.lua)

    Client->>API: PATCH /customers/:id { billing_controls: { spend_limits: [...] } }
    API->>API: Validate via CustomerBillingControlsParamsSchema\n(dedup feature_ids)
    API->>DB: CusService.update({ spend_limits: [...] })
    DB-->>API: OK
    API->>Cache: updateCachedCustomerData({ spend_limits: [...] })
    Cache->>Lua: JSON.stringify({ updates: { spend_limits: [...] } })
    Lua->>Cache: JSON.SET $.spend_limits
    Cache-->>API: { success: true, updated_fields: ["spend_limits"] }
    API-->>Client: Updated customer

    Note over API,DB: auto_topups left untouched when only spend_limits is provided\n(undefined skipped by Drizzle + JSON.stringify)
```
</details>

<sub>Last reviewed commit: 7d2b44a</sub>

> Greptile also left **2 inline comments** on this PR.

<!-- /greptile_comment -->
2026-03-12 12:05:29 +00:00
..
wip
2026-01-23 17:13:19 +00:00
2026-01-14 16:36:41 +00:00