PRD: Configuration & deletion policy
| Module | Commerce (CORE-03) | PRD ID | PRD-CFG-001 |
| Status | Shipped | Owner | Commerce squad |
| Date | 2026-04-03 | Version | v1.0 |
| Packages | @nx/commerce | URD | DEL · CFG |
TL;DR
Gives every merchant a configurable deletion policy - two flags that decide whether a category with linked products can be removed and whether deleting a category cascades to its products - backed by the encrypted per-merchant configuration store. Owners control delete behavior from the merchant, and every delete path (merchant, category, product, variant, sale-channel) routes through one guard so a mistaken delete cannot silently orphan or wipe content.
1. Context & Problem
Commerce already persists per-merchant settings - payment-provider credentials and integration config - as encrypted key-value entries via the ConfigurationService and the SettingsRepository (the Configuration / CFG area). What it lacks is a merchant-configurable answer to a destructive question: what should happen when an owner deletes a category that still has products, or deletes a product that has variants?
Without a policy, delete behavior is hard-coded and uniform. An owner cannot express "block deleting a category that still has products" versus "let the delete cascade to the products," and there is no single place that enforces the guard consistently across the merchant, category, product, variant, and sale-channel delete paths. For the HKD/SME bookkeeping KICKO targets, an accidental cascade - or an accidental orphan - is a data-loss incident.
This increment adds a per-merchant Deletion Policy (DEL) stored in the same encrypted settings store and wires a single guard into every merchant-content delete path.
2. Goals & Non-Goals
Goals
- Persist a per-merchant deletion policy with two flags -
strictCategoryDeletionandcascadeProductDeletion- defaulting to{ strict: true, cascade: false }. - Expose view and update of the policy on the merchant (
GET/POST /{id}/deletion-policy), gated byMerchant.findDeletionPolicy/Merchant.updateDeletionPolicypermissions. - Enforce the policy across delete flows: block category deletion when products exist and strict is on; cascade product deletion to variants when cascade is on; block deleting a product or variant that has already been sold.
- Reuse the encrypted per-merchant configuration store (
SettingsRepository) as the policy's home - no new column or table.
Non-Goals
- Hard-delete of any entity - all deletes remain soft-delete (URD C-03).
- Standalone category / sale-channel CRUD outside the merchant aggregate (Planned).
- A campaign hard-deletion flag - campaign deletion is out of scope.
- Cross-organization configuration sharing.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Guard coverage | 100% of merchant-content deletes (merchant, category, product, variant, sale-channel) route through the single deletion guard |
| Data safety | Zero accidental cascades or orphaned content; a sold product/variant is never hard-removed |
| Policy adoption | Merchants that customize the default policy (strict on / cascade off) where their workflow differs |
| Default correctness | New merchants start with { strict: true, cascade: false } with no manual setup |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Decide how deletions cascade for their merchant; view and update the policy |
| Manager | Delete categories/products within the safety rails the policy enforces |
| System (delete paths) | Apply the policy uniformly so no path can bypass the guard |
Core scenarios: an owner views the merchant's deletion policy → toggles strictCategoryDeletion / cascadeProductDeletion → subsequent category/product/variant/sale-channel deletes are evaluated against the policy → a guarded delete either blocks, cascades, or unlinks per the configured flags.
5. User Stories
- As an owner, I want a per-merchant deletion policy, so that delete behavior matches how my business treats categories and products.
- As an owner, I want to block deleting a category that still has products, so that I never lose products by removing their group.
- As an owner, I want the option to cascade a category delete to its products, so that I can clear out a whole group in one action when I intend to.
- As a manager, I want deletes that would destroy sold items to be refused, so that historical sales stay intact.
- As an owner, I want the policy stored with my existing encrypted settings, so that there is one home for merchant configuration.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Each merchant carries a configurable deletion policy with two flags, defaulting to { strictCategoryDeletion: true, cascadeProductDeletion: false } | URD-DEL-001 |
| FR-2 | strictCategoryDeletion: when true, block category deletion if the category has linked products; otherwise unlink | URD-DEL-002 |
| FR-3 | cascadeProductDeletion: when true, deleting a category cascades to its products | URD-DEL-003 |
| FR-4 | Owner can view and update the policy via GET / POST /{id}/deletion-policy, gated by Merchant.findDeletionPolicy / Merchant.updateDeletionPolicy | URD-DEL-004 |
| FR-5 | Product delete cascades to variants when cascade is on, else blocks if variants exist; asserts the product is not already sold | URD-DEL-002..003 |
| FR-6 | Variant and sale-channel delete assert not-sold and clean up dependencies | URD-DEL-002 |
| FR-7 | Merchant delete asserts no child merchants and no remaining content, then cleans up meta-links and settings | URD-DEL-001 |
| FR-8 | The policy is persisted per merchant through the encrypted settings store, grouped by area | URD-CFG-001..003 |
Full requirement text and acceptance criteria live in the Commerce URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | All deletes are soft-delete; a sold product/variant is never removed; deletes are guarded so content is never silently orphaned or cascaded against policy |
| Single guard | One service centralizes every merchant-content delete guard; no delete path bypasses it |
| Security | The policy is stored in the encrypted per-merchant settings store; sensitive configuration is encrypted at rest |
| Tenancy & authz | Policy view/update scoped per merchant and gated by Merchant.findDeletionPolicy / Merchant.updateDeletionPolicy |
| Consistency | Cascade and cleanup happen transactionally; a partial delete does not leave dangling references |
| i18n | User-facing labels/messages are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): the merchant settings area exposing the deletion-policy toggles, and the delete confirmations on category / product / variant / sale-channel that surface the guard outcome.
9. Data & Domain
| Entity | Role |
|---|---|
Merchant | Owns the deletion policy; delete paths for its content route through the guard |
| Deletion policy config | Two-flag policy (strictCategoryDeletion, cascadeProductDeletion) persisted per merchant |
| Settings store | Encrypted per-merchant key-value configuration (principalType = Merchant), grouped by area; home of the policy |
Category / Product / ProductVariant / SaleChannel | Content whose deletes are evaluated against the policy |
Conceptual only - full schema and invariants in the commerce domain model.
10. Dependencies & Assumptions
Depends on
- Configuration store (URD-CFG-001..003) - the encrypted per-merchant settings mechanism that stores the policy.
- Merchant (URD-MER) - the policy is owned by, and read from, the merchant.
- Categories / Products - the content the policy guards on delete.
Assumptions
- The encrypted settings store (
ConfigurationService,SettingsRepository) already exists and is reused, not rebuilt. - A merchant always resolves to a policy - the default
{ strict: true, cascade: false }applies when none is set. - Soldness of a product/variant can be determined at delete time.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| A cascade delete removes more than intended | Cascade defaults off; cascade only runs when the owner explicitly enables it |
| A delete path bypasses the guard | All merchant-content deletes route through the single guard service |
| Deleting content tied to historical sales | Sold products/variants are asserted not-sold and refused |
| Encrypted settings unavailable at delete time | Policy read falls back to the safe default (strict on, cascade off) |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (full management) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None - policy stored in the existing settings store; default applies when unset |
| Launch criteria | View/update endpoints permission-gated; strict-block, cascade, and not-sold guards verified across all delete paths |
| Monitoring | Blocked-delete rate, cascade-delete volume, guard-bypass alerts (should be zero) |
13. FAQ
What does the deletion policy default to? { strictCategoryDeletion: true, cascadeProductDeletion: false } - the safest combination: a category with products can't be deleted, and a category delete never cascades to products unless you turn cascade on.
Where is the policy stored? In the existing encrypted per-merchant settings store (SettingsRepository), not a new column or table.
Does the policy ever allow hard-deleting data? No - all deletes remain soft-delete (URD C-03). The policy only controls block / cascade / unlink behavior.
Can I delete a product that has already been sold? No - the guard asserts the product (and any variant) is not sold before removal, so historical sales stay intact.
Does this cover campaign deletion? No - campaign deletion is out of scope for this increment.
References
- URD: Commerce - Deletion Policy · Configuration
- Builds on: Merchant · Categories
- Module: Commerce - overview + traceability
- Developer: @nx/commerce · domain model