Add planning docs for trade-ins, returns, tax exemptions, cycle counts, POs, bundles, backorders, barcode labels, instrument sizing, warranties, maintenance schedules, gift cards, layaway, rental agreements, and in-home trials

This commit is contained in:
Ryan Moon
2026-03-27 20:53:01 -05:00
parent 750dcf4046
commit e7853f59f2
8 changed files with 1107 additions and 27 deletions

View File

@@ -101,6 +101,12 @@ enum
consolidated | split
is_active
boolean
Default true — soft delete flag. Financial history must be retained.
notes
text
@@ -125,6 +131,36 @@ timestamptz
When record was imported
tax_exempt_status
enum
none | pending | approved — default "none"
tax_exempt_certificate_number
varchar
State resale certificate or exemption number
tax_exempt_certificate_expiry
date
Nullable — expiration date of certificate
tax_exempt_approved_by
uuid FK
Employee who verified and approved exemption
tax_exempt_approved_at
timestamptz
When exemption was verified
created_at
timestamptz
@@ -217,6 +253,10 @@ created_at
timestamptz
updated_at
timestamptz
@@ -377,6 +417,12 @@ timestamptz
- Duplicate account detection on email and phone during creation
- Tax-exempt accounts must have a valid certificate number and expiry date before status can be set to "approved"
- Expired tax exemption certificates revert account to "pending" — staff prompted to collect updated certificate
- Tax exemption status changes logged in audit trail (who approved, when, certificate details)
# 5. Key Workflows

View File

@@ -224,7 +224,19 @@ timestamptz
Individual physical units for serialized items and rental fleet instruments.
id, product_id, company_id, location_id, serial_number,condition (new|excellent|good|fair|poor),status (available|sold|rented|in_repair|retired),purchase_date, purchase_cost, notes,legacy_id, created_at
id, product_id, company_id, location_id, serial_number,condition (new|excellent|good|fair|poor),status (available|sold|rented|on_trial|in_repair|layaway|lost|retired),purchase_date, purchase_cost, notes,legacy_id, created_at
### Status Values
Status | Description | Set By
available | In stock, ready for sale or rental | Default, return, restock
sold | Purchased by customer | Sale transaction
rented | Out on active rental contract | Rental activation
on_trial | Out with customer on in-home trial | In-home trial checkout (07_Domain_Sales_POS.md §9)
in_repair | In repair shop for service | Repair intake
layaway | Reserved for layaway customer, not available | Layaway creation (08_Domain_Payments_Billing.md §9)
lost | Unrecovered — trial, rental, or inventory discrepancy | Overdue escalation or cycle count
retired | Permanently removed from inventory | Manual retirement
@@ -924,4 +936,313 @@ timestamptz
- Parts cost always recorded at time of use — price changes do not affect historical records
- Negative qty_on_hand not permitted — system warns technician when stock is insufficient
- Negative qty_on_hand not permitted — system warns technician when stock is insufficient
# 8. Instrument Sizing
String instruments come in fractional sizes — critical for rentals, school orders, and customer matching. Size is tracked on both the product catalog and individual inventory units.
## 8.1 Size Values
Size | Instruments | Notes
4/4 (full) | Violin, viola, cello, bass, guitar | Default adult size
3/4 | Violin, cello, bass, guitar | Older students, smaller adults
1/2 | Violin, cello, bass | Intermediate students
1/4 | Violin, cello | Younger students
1/8 | Violin | Beginner young students
1/10 | Violin | Smallest common size
1/16 | Violin | Rare — very young students
15" | Viola | Measured in inches for viola
15.5" | Viola | Common intermediate
16" | Viola | Full-size standard
16.5" | Viola | Large full-size
## 8.2 Schema Changes
**product** — add column:
Column | Type | Notes
instrument_size | varchar | Nullable — only set for sized instruments. Free text to support both fractional (1/2) and inch (15.5") formats.
**inventory_unit** — add column:
Column | Type | Notes
instrument_size | varchar | Nullable — size of this specific physical unit. May differ from product default (e.g. product "Yamaha Model 5 Violin" has units in multiple sizes).
## 8.3 Business Rules
- Size is optional — only relevant for sized instruments (strings, some guitars)
- Product-level size is the default/catalog size — inventory units can override per-unit
- Size is searchable and filterable in product lists and POS lookup
- Rental matching: when a student needs a size, search filters by instrument_size
- Size changes on rental returns are common (student grew) — logged in rental history
# 9. Inventory Cycle Counts
Physical inventory reconciliation ensures system counts match actual shelf counts. Cycle counts can be full-store or targeted (by category, location area, or supplier).
## 9.1 count_session
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK | Which location is being counted
name | varchar | e.g. "Q1 2025 Full Count", "Guitar Room Spot Check"
count_type | enum | full | category | spot
status | enum | draft | in_progress | review | completed | cancelled
category_id | uuid FK | Nullable — set if count_type = category
started_by | uuid FK | Employee who initiated
started_at | timestamptz | When counting began
completed_at | timestamptz | When finalized
notes | text |
created_at | timestamptz |
## 9.2 count_entry
One row per product counted in a session. For serialized products, one row per unit.
Column | Type | Notes
id | uuid PK |
count_session_id | uuid FK |
product_id | uuid FK |
inventory_unit_id | uuid FK | Nullable — set for serialized items
expected_qty | integer | System qty at time count started
counted_qty | integer | Physical count entered by staff
variance | integer | counted_qty - expected_qty (computed)
counted_by | uuid FK | Employee who counted this item
counted_at | timestamptz | When this entry was recorded
notes | text | Explanation for variance
created_at | timestamptz |
## 9.3 count_adjustment
When a count session is completed, variances are applied as inventory adjustments. Each adjustment is an auditable record.
Column | Type | Notes
id | uuid PK |
count_session_id | uuid FK |
count_entry_id | uuid FK |
product_id | uuid FK |
inventory_unit_id | uuid FK | Nullable
previous_qty | integer | qty_on_hand before adjustment
adjusted_qty | integer | qty_on_hand after adjustment
adjustment_reason | enum | cycle_count | damaged | stolen | found | data_entry_error
approved_by | uuid FK | Manager who approved the adjustment
approved_at | timestamptz |
created_at | timestamptz |
## 9.4 Cycle Count Workflow
1. Manager creates count session — selects scope (full, category, or spot check)
2. System snapshots expected quantities for all products in scope
3. Staff count physical inventory — enter counts per product/unit
4. System calculates variances and flags discrepancies
5. Manager reviews variances — adds reason codes for each
6. Manager approves adjustments — qty_on_hand updated, adjustment records created
7. Session marked completed — immutable after completion
## 9.5 Business Rules
- Count sessions lock affected products from sale/receiving while in_progress — prevents count drift
- Variances above a configurable threshold (default: 5% or $50 value) require manager approval
- All adjustments are append-only audit records — never modified after creation
- Serialized items: count confirms unit is present and in expected status
- Completed sessions cannot be reopened — start a new session to re-count
- Spot checks do not lock inventory — used for quick verification without disrupting sales
# 10. Purchase Orders
Formal purchase order workflow extends the existing stock_receipt flow. POs track what was ordered, what was received, and flag discrepancies.
## 10.1 purchase_order
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK | Receiving location
po_number | varchar | Human-readable PO number (auto-generated)
supplier_id | uuid FK |
status | enum | draft | submitted | partial | received | cancelled
order_date | date | When PO was sent to supplier
expected_date | date | Expected delivery date
received_date | date | When fully received
subtotal | numeric(10,2) | Sum of line totals
shipping_cost | numeric(10,2) |
tax | numeric(10,2) |
total | numeric(10,2) |
notes | text | Internal notes or special instructions
created_by | uuid FK | Employee who created
created_at | timestamptz |
updated_at | timestamptz |
## 10.2 purchase_order_line
Column | Type | Notes
id | uuid PK |
purchase_order_id | uuid FK |
product_id | uuid FK |
supplier_sku | varchar | Supplier's SKU for this product
description | varchar | Line item description
qty_ordered | integer |
qty_received | integer | Updated as items arrive — default 0
unit_cost | numeric(10,2) | Agreed cost per unit
line_total | numeric(10,2) | qty_ordered * unit_cost
created_at | timestamptz |
## 10.3 Three-Way Match
When receiving against a PO, the system compares:
1. **PO line** — what was ordered (qty_ordered, unit_cost)
2. **Packing slip** — what supplier says they shipped (entered by staff at receiving)
3. **Physical count** — what actually arrived (counted by staff)
Discrepancies flagged: short shipment, over shipment, wrong item, cost mismatch.
## 10.4 PO → Stock Receipt Flow
- Receiving against a PO auto-creates stock_receipt records for each line received
- stock_receipt.purchase_order_id links receipt back to originating PO
- Partial receives update PO status to "partial" — remaining lines stay open
- Final receive updates PO status to "received"
## 10.5 Business Rules
- POs in draft status are editable — submitted POs are locked (create new revision if needed)
- Supplier auto-populated from product's preferred supplier if set
- Unit cost defaults to last stock_receipt cost for that product from that supplier
- PO approval workflow optional — configurable threshold requiring manager sign-off
- Cancelled POs retained for audit — soft cancel with reason code
- Reorder report: products below qty_reorder_point generate suggested PO lines grouped by preferred supplier
# 11. Product Bundles & Kits
Bundles group multiple products into a single sellable item at a package price. Common in music retail: instrument + case + accessories starter packs.
## 11.1 product_bundle
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
product_id | uuid FK | The bundle's parent product record (is_bundle = true)
created_at | timestamptz |
## 11.2 product_bundle_item
Column | Type | Notes
id | uuid PK |
bundle_id | uuid FK | References product_bundle
component_product_id | uuid FK | The included product
qty | integer | How many of this component per bundle (default 1)
sort_order | integer | Display order in bundle breakdown
created_at | timestamptz |
## 11.3 Schema Changes
**product** — add column:
Column | Type | Notes
is_bundle | boolean | Default false. True = this product is a bundle of other products.
## 11.4 Pricing Models
Model | Description
fixed | Bundle has a set price — component prices ignored at POS
sum_discount | Bundle price = sum of component prices minus a bundle discount (percent or fixed)
Bundle price is stored on the parent product.price field. The pricing model determines how the discount is displayed on receipts (single line vs itemized with discount).
## 11.5 Inventory Behavior
- Bundle does not have its own qty_on_hand — availability derived from component stock
- Bundle is "in stock" only if ALL components are in stock at required quantities
- Selling a bundle decrements each component product's qty_on_hand (or marks serialized units as sold)
- Stock receipt never targets a bundle directly — components are received individually
- Low stock alert triggers if any component falls below its reorder point
## 11.6 Business Rules
- A bundle component cannot itself be a bundle (no nesting)
- Bundles appear in POS search like any product — clearly labeled as bundle
- Receipt shows bundle name and price, with component breakdown below
- Returning a bundle returns all components — partial bundle returns handled as individual item returns
- Bundle price must be less than or equal to sum of component prices (enforced at creation)
# 12. Barcode Label Printing
Bulk label printing for inventory receiving, repricing, and cycle count preparation.
## 12.1 Label Templates
Template | Use Case | Content
standard_price | Shelf labels | SKU, name, price, barcode (UPC or SKU)
serialized | Individual units | Serial number, SKU, name, barcode (serial)
clearance | Sale items | SKU, name, original price (struck), sale price, barcode
receiving | Incoming stock | SKU, name, price, received date, barcode
bundle | Bundle items | Bundle name, bundle price, component list, barcode
## 12.2 Print Jobs
Label printing is triggered from:
- **Stock receipt** — auto-prompt to print labels for received items
- **Price change** — bulk print updated labels for repriced items
- **Cycle count prep** — print labels for products missing barcodes
- **Manual** — select products from inventory list, choose template, print
## 12.3 Technical
- Labels rendered server-side as PDF (ZPL support planned for thermal printers)
- Standard label sizes: 1.25" x 0.875" (Dymo), 2" x 1" (thermal roll), 4" x 6" (shipping)
- Barcode formats: Code 128 (SKU-based), UPC-A (manufacturer UPC), QR code (serial number)
- Print queue supports batching — up to 500 labels per job
# 13. Backorders
Customer order queue for out-of-stock items. Tracks demand and notifies customers when stock arrives.
## 13.1 backorder
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK |
product_id | uuid FK | What was requested
account_id | uuid FK | Who wants it
member_id | uuid FK | Nullable — specific member on account
qty | integer | Quantity requested
status | enum | pending | ordered | received | fulfilled | cancelled
purchase_order_line_id | uuid FK | Nullable — linked to PO when ordered from supplier
deposit_transaction_id | uuid FK | Nullable — if deposit collected
notes | text |
requested_date | date | When customer placed backorder
fulfilled_date | date | When customer received item
created_by | uuid FK | Employee who took the order
created_at | timestamptz |
updated_at | timestamptz |
## 13.2 Workflow
1. Customer wants an out-of-stock product — staff creates backorder
2. Optional: collect deposit via POS (deposit_transaction_id recorded)
3. When creating next PO for that supplier, backorder quantities surfaced as suggested lines
4. Stock receipt against PO triggers backorder match — status updated to "received"
5. Staff notified to contact customer — notification sent via preferred channel
6. Customer picks up item — backorder marked "fulfilled", deposit applied to sale
## 13.3 Business Rules
- Multiple backorders can exist for the same product — filled in request date order (FIFO)
- Backorder quantities included in reorder report alongside reorder point calculations
- Cancelled backorders refund any deposit collected
- Backorder demand report: shows products with pending backorders — informs purchasing decisions

View File

@@ -232,4 +232,133 @@ When billing_group is null, the rental has its own independent Stripe subscripti
- Final invoice generated for any partial month charges
- If damage found, repair ticket created automatically and linked to rental record
- If damage found, repair ticket created automatically and linked to rental record
# 7. Rental Agreement Contracts
Every rental requires a signed agreement before the instrument leaves the store. Agreements are generated from templates, capture all rental terms, and are stored as signed documents.
## 7.1 rental_agreement_template
Store-configurable contract templates. Most stores have one template per rental type, but can create variations for schools, events, etc.
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
name | varchar | e.g. "Standard Month-to-Month", "School RTO Agreement", "Short-Term Event Rental"
rental_type | enum | month_to_month | rent_to_own | short_term | lease_purchase | all
body | text | Contract body with merge fields (see §7.3)
requires_signature | boolean | Default true — some internal/school agreements may waive
requires_guardian_signature | boolean | Default true if member.is_minor
include_insurance_terms | boolean | Whether insurance section is included
include_rto_terms | boolean | Whether rent-to-own buyout terms are included
include_damage_policy | boolean | Whether damage/loss liability section is included
is_default | boolean | Default template for this rental_type
is_active | boolean |
created_at | timestamptz |
updated_at | timestamptz |
## 7.2 rental_agreement
A generated, signed agreement instance linked to a specific rental.
Column | Type | Notes
id | uuid PK |
company_id | uuid FK |
rental_id | uuid FK | The rental this agreement covers
template_id | uuid FK | Template used to generate
account_id | uuid FK |
member_id | uuid FK | Member receiving the instrument
agreement_number | varchar | Human-readable ID (auto-generated)
generated_body | text | Full rendered contract text with all merge fields resolved — immutable after signing
status | enum | draft | pending_signature | signed | voided
signed_at | timestamptz | When signature was captured
signer_name | varchar | Name of person who signed
signer_relationship | varchar | Nullable — e.g. "parent", "guardian", "self"
guardian_signed_at | timestamptz | Nullable — when guardian signed (if minor)
guardian_name | varchar | Nullable
signature_method | enum | in_store_tablet | in_store_paper | email | portal
signature_data | text | Nullable — base64 signature image for tablet/paper capture
document_url | varchar | Nullable — URL to stored PDF in object storage
ip_address | varchar | Nullable — for email/portal signatures
voided_reason | text | Nullable — why agreement was voided
voided_by | uuid FK | Nullable
created_at | timestamptz |
updated_at | timestamptz |
## 7.3 Merge Fields
Templates use merge fields that are resolved at generation time. The generated_body stores the fully resolved text so the agreement is a permanent record even if account details change later.
Field | Resolves To
{{account_name}} | account.name
{{account_email}} | account.email
{{account_phone}} | account.phone
{{account_address}} | account.address (formatted)
{{member_name}} | member.first_name + member.last_name
{{member_is_minor}} | "Yes" / "No"
{{instrument_description}} | product.name + product.brand + product.model
{{instrument_size}} | inventory_unit.instrument_size or product.instrument_size
{{serial_number}} | inventory_unit.serial_number
{{instrument_condition}} | inventory_unit.condition at rental start
{{rental_type}} | Formatted rental type name
{{monthly_rate}} | rental.monthly_rate
{{deposit_amount}} | rental.deposit_amount
{{start_date}} | rental.start_date (formatted)
{{end_date}} | rental.end_date or "Open-ended"
{{rto_purchase_price}} | rental.rto_purchase_price (if RTO)
{{rto_equity_percent}} | rental.rto_equity_percent (if RTO)
{{company_name}} | company.name
{{company_address}} | company.address
{{company_phone}} | company.phone
{{today_date}} | Current date
{{agreement_number}} | rental_agreement.agreement_number
## 7.4 Agreement Workflow
1. Staff creates rental — system selects default template for rental_type (or staff picks one)
2. Agreement generated: merge fields resolved, generated_body populated, status = "draft"
3. Staff reviews agreement on screen — can edit before finalizing if needed
4. Signature capture:
a. **In-store tablet**: customer signs on screen, signature_data captured as base64
b. **In-store paper**: staff prints, customer signs physical copy, staff scans/uploads
c. **Email**: agreement emailed to account email with secure signing link
d. **Portal**: customer signs via self-service portal (if MOD-PORTAL licensed)
5. Agreement status updated to "signed" — PDF generated and stored
6. Rental cannot activate until agreement is signed (unless overridden by manager)
7. Signed agreement PDF available on rental record, account history, and customer portal
## 7.5 Guardian Signatures
- If member.is_minor = true, agreement requires guardian signature in addition to (or instead of) member signature
- Guardian must be an adult member on the same account, or signer_relationship recorded for non-member guardian
- Both signature fields must be completed before agreement is fully signed
- Email/portal signing flow sends to account primary email (assumed to be parent/guardian)
## 7.6 Agreement Delivery
- **Print**: generated as PDF, sent to receipt printer or standard printer
- **Email**: PDF attached to email sent to account.email
- **Portal**: PDF available in customer portal under "My Agreements"
- **All signed agreements**: stored in object storage (S3-compatible), URL recorded on rental_agreement.document_url
## 7.7 Voiding and Re-signing
- Voiding an agreement requires reason and manager approval
- Voiding does not cancel the rental — but rental cannot remain active without a signed agreement
- Common void reasons: incorrect terms, wrong instrument, customer requested change
- After voiding, a new agreement is generated from template (or modified) and signed
- Voided agreements retained for audit — never deleted
## 7.8 Business Rules
- Rental cannot activate without a signed agreement (manager override available with reason logged)
- Agreement text is immutable after signing — edits require void and re-sign
- generated_body is the legal record — template changes do not affect existing agreements
- Signature data retained for the life of the agreement (minimum 7 years per record retention policy)
- Agreement PDF regenerated on demand from generated_body + signature_data — not dependent on template
- Schools with bulk rentals (MOD-BATCH): single master agreement can cover multiple instruments for a school account
- Short-term rentals may use a simplified template with fewer terms
- Agreement history visible on account record — all past and current agreements listed

View File

@@ -432,4 +432,124 @@ Margin on flat-rate services — e.g. are rehair rates covering costs
- Repair complete status triggers customer notification via preferred channel
- Delivered status set by delivery domain completion — not manually
- Delivered status set by delivery domain completion — not manually
# 7. Warranty Tracking
Warranties are tracked per inventory unit — both manufacturer warranties and store-offered extended warranties. Warranty status is surfaced during repair intake to determine billing responsibility.
## 7.1 warranty
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
inventory_unit_id | uuid FK | The specific instrument covered
account_id | uuid FK | Account that owns the warranty
warranty_type | enum | manufacturer | extended | store
provider | varchar | Warranty provider name (e.g. "Yamaha", "Store Protection Plan")
coverage_description | text | What's covered — free text or template
start_date | date | When coverage begins
end_date | date | When coverage expires
purchase_price | numeric(10,2) | Nullable — price paid for extended warranty (0 for manufacturer)
transaction_id | uuid FK | Nullable — sale transaction where warranty was purchased
status | enum | active | expired | claimed | voided
max_claims | integer | Nullable — max number of claims allowed (null = unlimited)
claims_used | integer | Default 0
notes | text |
created_at | timestamptz |
updated_at | timestamptz |
## 7.2 warranty_claim
Column | Type | Notes
id | uuid PK |
warranty_id | uuid FK |
repair_ticket_id | uuid FK | The repair covered by this claim
claim_date | date |
issue_description | text | What went wrong
resolution | text | How it was resolved
claim_amount | numeric(10,2) | Cost covered by warranty
status | enum | submitted | approved | denied | completed
denied_reason | text | Nullable — why claim was denied
processed_by | uuid FK | Employee who processed
created_at | timestamptz |
## 7.3 Warranty Flow
1. **At sale**: staff optionally adds extended warranty to transaction — warranty record created linked to inventory_unit
2. **Manufacturer warranty**: auto-created when new serialized item is sold, using manufacturer's standard warranty period
3. **At repair intake**: system checks if instrument has active warranty — surfaces to staff
4. **If under warranty**: repair ticket linked to warranty claim — customer not billed (or reduced billing)
5. **Claim processing**: store submits claim to manufacturer for reimbursement if applicable
6. **Expiry**: system tracks end_date — warranty status auto-updated to "expired"
## 7.4 Business Rules
- Warranty checked automatically during repair intake — staff sees "UNDER WARRANTY" or "WARRANTY EXPIRED"
- Extended warranties are sold as a line item on the original sale transaction
- Manufacturer warranty periods are configurable per brand (e.g. Yamaha = 5 years, generic = 1 year)
- Warranty claims reduce repair ticket billing — covered amount shown on invoice as "warranty credit"
- Voided warranties (e.g. customer damage outside coverage) require reason code and manager approval
- Warranty report: active warranties by expiry date, claim history, claim approval rate
# 8. Maintenance Schedules
Preventive maintenance recommendations and reminders per instrument. Helps stores build recurring service revenue and keeps customer instruments in good condition.
## 8.1 maintenance_schedule_template
Company-configurable templates define recommended maintenance intervals by instrument type.
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
instrument_type | varchar | e.g. "violin", "trumpet", "clarinet", "flute"
service_name | varchar | e.g. "Bow Rehair", "Valve Oil & Cleaning", "Pad Replacement Check"
interval_months | integer | Recommended interval between services
description | text | Customer-facing description of what the service includes
estimated_cost | numeric(10,2) | Typical cost — for customer expectation setting
sort_order | integer | Display order
is_active | boolean |
created_at | timestamptz |
## 8.2 maintenance_reminder
Tracks scheduled reminders for specific instruments owned by customers.
Column | Type | Notes
id | uuid PK |
company_id | uuid FK |
account_id | uuid FK |
member_id | uuid FK | Nullable — specific member
inventory_unit_id | uuid FK | Nullable — specific instrument if tracked
template_id | uuid FK | Which maintenance template this follows
instrument_description | varchar | Fallback description if no inventory_unit linked
last_service_date | date | When this service was last performed
next_due_date | date | Computed: last_service_date + interval_months
status | enum | upcoming | due | overdue | completed | dismissed
notification_sent | boolean | Whether customer has been notified
repair_ticket_id | uuid FK | Nullable — linked to repair ticket when service is scheduled
created_at | timestamptz |
updated_at | timestamptz |
## 8.3 Maintenance Workflow
1. When a repair is completed, system suggests creating maintenance reminders based on instrument type
2. Reminders auto-calculated: next_due_date = completed_date + template.interval_months
3. Daily job checks for due/overdue reminders — generates notification queue
4. Customer notified via preferred channel (email, SMS, portal notification)
5. When customer brings instrument in, staff sees pending maintenance recommendations
6. Completed service updates the reminder: last_service_date = today, next recalculated
## 8.4 Business Rules
- Maintenance reminders are suggestions, not obligations — customer can dismiss
- Templates are seeded with common defaults, fully customizable per store
- Reminders auto-created for rental fleet instruments — store maintains own fleet on schedule
- Overdue reminders escalate: due → 30 days overdue (second notice) → 90 days (final notice, then dismissed)
- Maintenance history visible on customer account and instrument record
- Revenue report: maintenance service revenue, conversion rate from reminder to repair ticket

View File

@@ -228,4 +228,255 @@ Account charge
Internal
Charges to account balance — billed later
Charges to account balance — billed later
# 6. Trade-In Workflow
Trade-ins are a core music retail workflow. A customer brings in a used instrument and receives credit toward a purchase. The trade-in creates both an inventory intake (new used product) and a credit applied to a transaction.
## 6.1 trade_in
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK |
account_id | uuid FK | Nullable — walk-in trade-ins allowed
appraised_by | uuid FK | Employee who appraised
instrument_description | varchar | What the customer is trading in
serial_number | varchar | Nullable
condition | enum | excellent | good | fair | poor
appraisal_value | numeric(10,2) | What store offers the customer
credit_applied | numeric(10,2) | Amount applied to transaction (may equal appraisal or less if partial)
transaction_id | uuid FK | Nullable — the sale transaction where credit was applied
product_id | uuid FK | Nullable — the product record created for the traded-in item
inventory_unit_id | uuid FK | Nullable — the inventory unit created (if serialized)
status | enum | appraised | accepted | declined | sold
notes | text | Condition details, customer negotiation notes
appraised_at | timestamptz |
created_at | timestamptz |
## 6.2 Trade-In Flow
1. Customer brings instrument — staff appraises condition and fair market value
2. Staff enters trade-in: description, serial, condition, appraisal value
3. Customer accepts or declines the offer
4. If accepted:
a. New product/inventory_unit created for the traded-in instrument (used, condition noted)
b. Trade-in credit applied to the current or future transaction
c. Transaction shows trade-in credit as a line item (negative amount)
5. Traded-in instrument enters inventory as available for resale
## 6.3 Business Rules
- Appraisal value is a negotiation — staff can adjust within min/max guidelines
- Trade-in credit can exceed purchase price — difference paid as store credit (not cash refund)
- Trade-in without immediate purchase: credit stored on account as store credit balance
- Traded-in items default to "used" condition and get a new product record (not merged with new inventory)
- Trade-in appraisal records are immutable — if value changes, create new appraisal
- Trade-in value reported separately from sales discount for accounting purposes
- Manager approval required for trade-in value above configurable threshold
# 7. Returns & Exchanges
Structured returns workflow beyond simple refund transactions. Handles RMA tracking, reason codes, restocking, and return window enforcement.
## 7.1 return_request
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK |
return_number | varchar | Human-readable RMA number (auto-generated)
original_transaction_id | uuid FK | The sale being returned against
account_id | uuid FK | Nullable — anonymous returns allowed with receipt
status | enum | pending | approved | completed | denied
return_type | enum | refund | exchange | store_credit
initiated_by | uuid FK | Employee processing return
approved_by | uuid FK | Nullable — manager if approval required
reason_code | enum | defective | wrong_item | changed_mind | duplicate | other
reason_detail | text | Free-text explanation
restocking_fee | numeric(10,2) | Default 0 — configurable per category
refund_amount | numeric(10,2) | Amount returned to customer (after restocking fee)
refund_method | enum | original_payment | cash | store_credit
refund_transaction_id | uuid FK | The refund transaction created
created_at | timestamptz |
updated_at | timestamptz |
## 7.2 return_line_item
Column | Type | Notes
id | uuid PK |
return_request_id | uuid FK |
original_line_item_id | uuid FK | Line item from original transaction
product_id | uuid FK |
inventory_unit_id | uuid FK | Nullable — for serialized items
qty_returned | integer |
unit_price | numeric(10,2) | Price at time of original sale
condition_returned | enum | new | excellent | good | fair | poor | defective
restock | boolean | Whether item goes back to available inventory
created_at | timestamptz |
## 7.3 Return Policies
return_policy — configurable per company, overridable per category:
Column | Type | Notes
id | uuid PK |
company_id | uuid FK |
category_id | uuid FK | Nullable — null = company-wide default
return_window_days | integer | Days after purchase returns are accepted (e.g. 30)
restocking_fee_percent | numeric(5,2) | Default restocking fee (e.g. 15%)
requires_receipt | boolean | Whether original receipt/transaction required
requires_approval | boolean | Whether manager approval needed
exchange_only | boolean | Some categories only allow exchange, not refund
is_active | boolean |
created_at | timestamptz |
## 7.4 Return Workflow
1. Customer requests return — staff looks up original transaction
2. System checks return window — warns if outside policy (manager can override)
3. Staff selects items being returned, enters reason code and condition
4. System calculates refund amount (original price minus restocking fee if applicable)
5. Manager approves if required by policy or amount threshold
6. Refund processed: original payment method, cash, or store credit
7. Returned items: restocked (condition updated) or marked defective (removed from available)
## 7.5 Exchange Flow
1. Return initiated with return_type = exchange
2. Customer selects replacement item(s)
3. System calculates difference — customer pays overage or receives credit for underage
4. Single transaction captures both the return credit and the new sale
5. Both the return and new sale are linked for reporting
## 7.6 Business Rules
- Return window enforced at POS — staff sees clear "within policy" / "outside policy" indicator
- Outside-policy returns require manager override with reason
- Defective items never restocked — routed to repair assessment or disposal
- Serialized items: inventory unit status reverted from "sold" to "available" on restock
- Non-serialized items: qty_on_hand incremented on restock
- Refund to original payment method preferred — Stripe refund API called for card transactions
- Store credit refunds create a credit balance on the account
- No cash refunds above configurable threshold without manager approval
- Return fraud tracking: system flags accounts with high return frequency
# 8. Tax Exemptions at POS
Tax-exempt customers (schools, churches, resellers) are common in music retail. Tax exemption is checked at transaction time and the exemption is recorded on each qualifying transaction.
## 8.1 Transaction Tax Flow
1. Transaction started — account attached (or anonymous)
2. System checks account.tax_exempt_status:
- If "approved": tax automatically set to $0.00 on all eligible lines
- If "pending" or "none": standard tax calculation applies
3. Tax exemption displayed on POS screen — staff sees "TAX EXEMPT" indicator
4. Receipt and invoice show $0.00 tax with exemption reference number
5. Tax-exempt transactions flagged in reporting for audit compliance
## 8.2 Manual Override
- Staff can manually mark a transaction as tax-exempt (e.g. walk-in school purchaser with certificate)
- Manual exemptions require: certificate number entry and manager approval
- Manual exemptions logged in tax_exemption_audit for compliance
## 8.3 Business Rules
- Tax exemption applies to the entire transaction, not individual line items
- Resale certificates have expiration dates — system warns when nearing expiry
- Expired certificates block auto-exemption until renewed — staff can override with manager approval
- Tax-exempt transaction count and value reported monthly for state compliance
- Rental transactions for schools are tax-exempt if the school account is exempt
# 9. In-Home Trials
In-home trials let a customer take an instrument home to try before committing to a purchase. Common for higher-value instruments (guitars, violins, horns) where feel, sound, and fit matter. The instrument leaves the store, so precise inventory tracking is critical.
## 9.1 instrument_trial
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK | Originating location
account_id | uuid FK | Customer taking the trial
member_id | uuid FK | Nullable — specific member
inventory_unit_id | uuid FK | The specific instrument being trialed
trial_number | varchar | Human-readable ID (auto-generated)
status | enum | active | returned | converted | overdue | cancelled
checkout_date | date | When instrument left the store
due_date | date | When instrument must be returned
returned_date | date | Nullable — when actually returned
duration_days | integer | Trial length (default configurable, e.g. 3-7 days)
deposit_amount | numeric(10,2) | Nullable — optional hold or deposit collected
deposit_transaction_id | uuid FK | Nullable — POS transaction for deposit
condition_out | enum | new | excellent | good | fair | poor — condition at checkout
condition_in | enum | Nullable — condition at return
checked_out_by | uuid FK | Employee who processed checkout
checked_in_by | uuid FK | Nullable — employee who processed return
sale_transaction_id | uuid FK | Nullable — if trial converted to sale
notes | text |
created_at | timestamptz |
updated_at | timestamptz |
## 9.2 Inventory Unit Status
**inventory_unit.status** — add value:
Value | Notes
on_trial | Instrument is out with a customer on an in-home trial. Not available for sale, rental, or display.
This status ensures the instrument is accounted for in all inventory views. Staff can see exactly which instruments are out on trial, with whom, and when they're due back.
## 9.3 Trial Workflow
1. Customer interested in an instrument — staff creates trial, selects inventory unit
2. Condition documented at checkout (condition_out)
3. Optional: deposit collected via POS (hold against card or cash deposit)
4. inventory_unit.status set to "on_trial"
5. Customer takes instrument home
6. Daily job checks for overdue trials — flags on dashboard, notifies staff
7. Customer returns instrument:
a. Condition assessed at return (condition_in)
b. inventory_unit.status reverted to "available" (or "in_repair" if damaged)
c. Deposit refunded if collected
8. **Conversion**: customer decides to buy — trial marked "converted", sale transaction created, deposit applied as payment
## 9.4 Multiple Trials
- A customer may trial multiple instruments simultaneously (e.g. comparing two violins)
- Each instrument gets its own trial record
- Staff can see all active trials per customer on the account view
- Common scenario: customer takes two, returns one, buys the other
## 9.5 Overdue Handling
- Trial due_date is firm — system escalates overdue trials:
- Day of due date: reminder notification sent to customer
- 1 day overdue: staff alerted on dashboard
- 3 days overdue: manager notified, customer contacted directly
- 7+ days overdue: escalation to store owner, deposit may be charged
- Overdue trials appear prominently on the daily operations dashboard
- If deposit was collected, store can charge the deposit after configurable overdue period
## 9.6 Business Rules
- Trial requires an account — no anonymous trials (need contact info for follow-up and liability)
- Only serialized inventory units can go on trial (need serial number tracking)
- Instrument must be in "available" status to start a trial
- Maximum concurrent trials per account configurable (default: 3)
- Maximum trial duration configurable per company (default: 7 days, max: 30 days)
- Condition documented at checkout and return — discrepancies flagged for review
- Deposit optional but recommended for high-value instruments — threshold configurable
- Trial history visible on both the account record and the inventory unit record
- Conversion rate report: trials started vs converted to sale, by instrument category and employee
- Lost instrument on trial: if not returned and unresolved, inventory_unit.status set to "lost", deposit charged, account flagged

View File

@@ -198,4 +198,183 @@ The billing service checks `store.payment_processor` to determine the flow:
id, event_id (Stripe's ID), event_type, payload (jsonb),processed_at, status (received|processed|failed), error_message, created_at
All incoming webhook events are stored before processing. This enables replay if processing fails.
All incoming webhook events are stored before processing. This enables replay if processing fails.
# 8. Gift Cards & Store Credits (MOD-GIFTCARD)
Gift cards and store credits are a premium module. Gift cards are purchasable/redeemable stored-value instruments. Store credits are system-issued balances (from returns, trade-ins, or adjustments).
## 8.1 gift_card
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
card_number | varchar | Unique card identifier (printed on physical card or emailed)
pin | varchar | Nullable — optional PIN for security (hashed)
type | enum | physical | digital
initial_balance | numeric(10,2) | Original loaded amount
current_balance | numeric(10,2) | Remaining balance
status | enum | active | redeemed | expired | disabled
purchased_by_account_id | uuid FK | Nullable — who bought it
recipient_email | varchar | Nullable — for digital cards
recipient_name | varchar | Nullable
purchase_transaction_id | uuid FK | The sale transaction where card was purchased
expiry_date | date | Nullable — some jurisdictions prohibit expiry
is_reloadable | boolean | Default false
created_at | timestamptz |
updated_at | timestamptz |
## 8.2 gift_card_transaction
Append-only ledger of all balance changes on a gift card.
Column | Type | Notes
id | uuid PK |
gift_card_id | uuid FK |
transaction_type | enum | purchase | reload | redemption | refund | adjustment | expiry
amount | numeric(10,2) | Positive for loads, negative for redemptions
balance_after | numeric(10,2) | Running balance after this transaction
related_transaction_id | uuid FK | Nullable — the POS transaction where redeemed/purchased
performed_by | uuid FK | Employee
notes | text |
created_at | timestamptz |
## 8.3 store_credit
Store credits are account-level balances issued by the system, not purchasable products.
Column | Type | Notes
id | uuid PK |
company_id | uuid FK |
account_id | uuid FK | Account that holds the credit
reason | enum | return | trade_in | adjustment | promotion | gift_card_conversion
original_amount | numeric(10,2) |
remaining_balance | numeric(10,2) |
issued_by | uuid FK | Employee who issued
related_return_id | uuid FK | Nullable — if from a return
related_trade_in_id | uuid FK | Nullable — if from a trade-in
expires_at | timestamptz | Nullable — configurable expiry
status | enum | active | depleted | expired | voided
created_at | timestamptz |
updated_at | timestamptz |
## 8.4 store_credit_transaction
Append-only ledger for store credit balance changes.
Column | Type | Notes
id | uuid PK |
store_credit_id | uuid FK |
transaction_type | enum | issued | applied | adjustment | expired | voided
amount | numeric(10,2) |
balance_after | numeric(10,2) |
related_transaction_id | uuid FK | Nullable
performed_by | uuid FK |
created_at | timestamptz |
## 8.5 POS Integration
- Gift card sold as a product at POS — triggers gift_card creation with initial balance
- Gift card redemption is a payment method: customer presents card, balance checked, amount deducted
- Partial redemption supported — remaining balance stays on card
- Split tender: gift card covers part, remaining on card/cash
- Store credit auto-applied: when account has credit balance, POS prompts "Apply $X.XX store credit?"
- Both gift card and store credit balances visible on account summary
## 8.6 Business Rules
- Gift card numbers generated with check digit to prevent typos
- Physical cards activated at POS — not active until purchased (prevents theft of unactivated cards)
- Digital cards emailed immediately with card number and optional message
- Gift card balance inquiries available at POS and customer portal
- Expiry rules vary by jurisdiction — configurable per company, default: no expiry
- Store credits cannot be cashed out — applied to purchases only
- All balance changes are append-only ledger entries — no direct balance edits
- Gift card liability tracked for accounting: total outstanding balances reported as liability
- Reloadable gift cards allow additional value to be added after purchase
# 9. Layaway & Payment Plans (MOD-LAYAWAY)
Layaway allows a customer to reserve an item with a deposit and pay it off over time. The item is held (not available for sale) until fully paid.
## 9.1 layaway
Column | Type | Notes
id | uuid PK |
company_id | uuid FK | Tenant scoping
location_id | uuid FK |
layaway_number | varchar | Human-readable ID (auto-generated)
account_id | uuid FK |
status | enum | active | completed | defaulted | cancelled
total_price | numeric(10,2) | Full price of items on layaway
deposit_amount | numeric(10,2) | Initial deposit collected
amount_paid | numeric(10,2) | Total paid to date (including deposit)
balance_remaining | numeric(10,2) | total_price - amount_paid
payment_frequency | enum | weekly | biweekly | monthly
next_payment_date | date | Next scheduled payment
payment_amount | numeric(10,2) | Scheduled payment amount per period
max_duration_days | integer | Maximum days to complete layaway (e.g. 90)
expires_at | date | Deposit date + max_duration_days
cancellation_fee | numeric(10,2) | Fee charged if customer cancels (configurable)
notes | text |
created_by | uuid FK |
created_at | timestamptz |
updated_at | timestamptz |
## 9.2 layaway_item
Column | Type | Notes
id | uuid PK |
layaway_id | uuid FK |
product_id | uuid FK |
inventory_unit_id | uuid FK | Nullable — for serialized items, unit is reserved
description | varchar |
qty | integer |
unit_price | numeric(10,2) |
line_total | numeric(10,2) |
created_at | timestamptz |
## 9.3 layaway_payment
Column | Type | Notes
id | uuid PK |
layaway_id | uuid FK |
transaction_id | uuid FK | The POS transaction for this payment
amount | numeric(10,2) |
balance_after | numeric(10,2) | Remaining balance after payment
payment_number | integer | 1, 2, 3... sequential
is_deposit | boolean | True for initial deposit
paid_at | timestamptz |
created_at | timestamptz |
## 9.4 Layaway Workflow
1. Customer selects items — staff creates layaway with deposit (minimum deposit configurable, e.g. 20%)
2. Serialized items: inventory_unit.status set to "layaway" (held, not available for sale)
3. Non-serialized items: qty reserved (decremented from available)
4. Customer makes scheduled payments at POS — each payment recorded as layaway_payment
5. When balance reaches $0: layaway completed, items released to customer, standard sale transaction created
6. Missed payments: system sends reminders at configurable intervals
7. Default: after configurable missed payments or past expiry — manager reviews for cancellation
## 9.5 Cancellation
- Customer-initiated cancellation: deposit refunded minus cancellation fee, items returned to available inventory
- Default cancellation (non-payment): same as above, but store may retain more of deposit per policy
- Cancellation fee configurable per company — can be fixed amount or percentage of amount paid
- All payments refunded via original payment method or store credit (store's choice per policy)
## 9.6 Business Rules
- Minimum deposit enforced (configurable — default 20% of total)
- Items on layaway are held — not available for sale to other customers
- Price locked at layaway creation — price changes don't affect existing layaways
- Payment reminders sent before due date (configurable: 3 days before default)
- Overdue payments flagged on dashboard — staff follows up
- Layaway report: active layaways, total held value, upcoming payments, overdue accounts
- Maximum layaway duration configurable per company (default 90 days)
- Layaway items can be exchanged (size swap) with manager approval — price difference adjusted

View File

@@ -174,6 +174,18 @@ API access — REST API for third-party integrations, webhooks, data export. Rat
Technical integrations
MOD-GIFTCARD
Gift cards and store credits — physical and digital gift cards, store credit balances, redemption at POS, balance tracking.
Stores wanting gift card revenue
MOD-LAYAWAY
Layaway and payment plans — item reservation with deposit, scheduled payments, hold management.
Stores with high-value instruments
## 2.3 Payment Processor Modules
@@ -240,6 +252,14 @@ MOD-API
None — can be added to any configuration
MOD-GIFTCARD
None — can be added to any configuration
MOD-LAYAWAY
None — can be added to any configuration
# 3. Self-Hosted — Perpetual Licensing
@@ -356,6 +376,22 @@ $80
REST API access
MOD-GIFTCARD
$300
$60
Gift cards & store credits
MOD-LAYAWAY
$300
$60
Layaway & payment plans
PAY-STRIPE
$300

View File

@@ -108,26 +108,26 @@ Consignment | 9/10 | Commission tracking, settlement workflow, accounting
Sales commission | 8/10 | Rate hierarchy, per-employee, category overrides
Personnel | 8/10 | Time clock, scheduling, time off, payroll export
## 4.2 Missing — High Priority (Add Before Launch)
## 4.2 Previously Missing — Now Planned (added to domain docs)
Feature | Impact | Notes
Gift cards / store credits | High | Common customer request. Needs gift_card table, balance tracking, POS redemption.
Trade-in workflow | High | Huge in music retail. Customer brings used instrument, gets credit toward purchase. Needs appraisal + credit + inventory intake flow.
Tax exemptions | High | Schools, churches, resellers are core customers. Need tax_exempt flag on account + resale certificate tracking.
Inventory cycle counts | High | Physical inventory reconciliation. Need count sessions, variance tracking, adjustment audit trail.
Returns/exchanges workflow | High | Structured beyond just refund transactions. Need RMA, reason codes, return window policies, restocking.
Purchase orders | Medium | PO generation, receiving against PO, three-way match. stock_receipt is a start but needs formal PO workflow.
Feature | Planning Doc | Module
Trade-in workflow | 07_Domain_Sales_POS.md §6 | Core
Tax exemptions | 02_Domain_Accounts_Customers.md + 07_Domain_Sales_POS.md §8 | Core
Inventory cycle counts | 03_Domain_Inventory.md §9 | Core
Returns/exchanges workflow | 07_Domain_Sales_POS.md §7 | Core
Purchase orders | 03_Domain_Inventory.md §10 | Core
Product bundles / kits | 03_Domain_Inventory.md §11 | Core
Barcode label printing | 03_Domain_Inventory.md §12 | Core
Backorders | 03_Domain_Inventory.md §13 | Core
In-home trials | 07_Domain_Sales_POS.md §9 | Core
Instrument sizing | 03_Domain_Inventory.md §8 | Core
Rental agreement contracts | 04_Domain_Rentals.md §7 | MOD-RENTALS
Warranty tracking | 06_Domain_Repairs.md §7 | MOD-REPAIRS
Maintenance schedules | 06_Domain_Repairs.md §8 | MOD-REPAIRS
Gift cards / store credits | 08_Domain_Payments_Billing.md §8 | MOD-GIFTCARD (premium)
Layaway / payment plans | 08_Domain_Payments_Billing.md §9 | MOD-LAYAWAY (premium)
## 4.3 Missing — Medium Priority
Feature | Notes
Layaway / payment plans | Hold item with partial payment, release on full payment
Product bundles / kits | Instrument + case + accessories at bundle price
Barcode label printing | Bulk label printing for inventory
Backorders | Customer order queue for out-of-stock items
Warranty tracking | Warranty period, claim tracking, extended warranty options
## 4.4 Missing — Lower Priority / Future
## 4.3 Still Missing — Lower Priority / Future
Feature | Notes
Customer loyalty / rewards | Point accumulation and redemption
@@ -135,9 +135,7 @@ E-commerce / online store | Product catalog, cart, online payments
Shipping integration | Carrier APIs, label printing, tracking
Multi-currency | Exchange rates, currency-specific pricing
## 4.5 Music-Specific Gaps
## 4.4 Music-Specific Gaps — Remaining
Feature | Notes
Instrument sizing | 1/4, 1/2, 3/4, full — critical for string instrument rentals. Need size field on product/inventory_unit.
Rental insurance certificates | Certificate generation for rental instruments. Common parent request.
Instrument maintenance schedules | Preventive maintenance recommendations and reminders per instrument.