15 KiB
Music Store Management Platform
Domain Design: Sales & Point of Sale
1. Overview
The Sales & POS domain handles all in-person and counter transactions — retail sales, deposits, repair payments, rental fees, and account charges. It integrates with Stripe Terminal for card-present payments and Stripe Elements for keyed entry.
2. Transaction Types
Type
Description
sale
Retail sale of product — instruments, accessories, supplies
repair_payment
Payment collected at repair ticket pickup
rental_deposit
Security deposit collected at rental start
account_payment
Payment against outstanding account balance
refund
Return of payment — full or partial
3. Database Schema
3.1 transaction
Column
Type
Notes
id
uuid PK
company_id
uuid FK
Tenant scoping
location_id
uuid FK
Physical location where transaction occurred
transaction_number
varchar
Human-readable ID
account_id
uuid FK
Nullable for anonymous cash sales
transaction_type
enum
sale|repair_payment|rental_deposit|account_payment|refund
status
enum
pending|completed|voided|refunded
subtotal
numeric(10,2)
Before tax and discounts
discount_total
numeric(10,2)
Total discounts applied
tax_total
numeric(10,2)
total
numeric(10,2)
Final amount charged
payment_method
enum
card_present|card_keyed|cash|check|account_charge
stripe_payment_intent_id
varchar
For card transactions
stripe_terminal_reader_id
varchar
Which reader was used
processed_by
uuid FK
Employee who processed
notes
text
created_at
timestamptz
3.2 transaction_line_item
id, transaction_id, product_id (nullable), inventory_unit_id (nullable),description, qty, unit_price, discount_amount, discount_reason,tax_rate, line_total, created_at
3.3 discount
id, company_id, location_id, name, discount_type (percent|fixed),discount_value, applies_to (order|line_item|category),requires_approval_above (threshold amount), is_active,valid_from, valid_until, created_at
3.4 discount_audit
id, transaction_id, transaction_line_item_id, discount_id,applied_by (employee_id), approved_by (employee_id, nullable),original_amount, discounted_amount, reason, created_at
4. Discount & Adjustment Rules
-
Discounts are applied at line item or order level
-
Discounts exceeding manager threshold require approval — logged in discount_audit
-
Manual price overrides treated as discounts — require reason code
-
Promotional codes validated against discount table before application
-
All discounts appear as line items on invoice — original price shown
-
Discount audit records are immutable — no deletion permitted
5. Payment Methods
Method
Integration
Notes
Card present
Stripe Terminal WiFi
Desktop — WisePOS E reader
Card present (BT)
Stripe Terminal BT
iOS — Bluetooth reader at conventions
Card keyed
Stripe Elements
Desktop — PCI safe, card data never touches app
Cash
Manual entry
Change calculated, drawer tracked
Check
Manual entry
Check number recorded
Account charge
Internal
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
- Customer brings instrument — staff appraises condition and fair market value
- Staff enters trade-in: description, serial, condition, appraisal value
- Customer accepts or declines the offer
- 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)
- 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
- Customer requests return — staff looks up original transaction
- System checks return window — warns if outside policy (manager can override)
- Staff selects items being returned, enters reason code and condition
- System calculates refund amount (original price minus restocking fee if applicable)
- Manager approves if required by policy or amount threshold
- Refund processed: original payment method, cash, or store credit
- Returned items: restocked (condition updated) or marked defective (removed from available)
7.5 Exchange Flow
- Return initiated with return_type = exchange
- Customer selects replacement item(s)
- System calculates difference — customer pays overage or receives credit for underage
- Single transaction captures both the return credit and the new sale
- 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
- Transaction started — account attached (or anonymous)
- 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
- Tax exemption displayed on POS screen — staff sees "TAX EXEMPT" indicator
- Receipt and invoice show $0.00 tax with exemption reference number
- 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
- Customer interested in an instrument — staff creates trial, selects inventory unit
- Condition documented at checkout (condition_out)
- Optional: deposit collected via POS (hold against card or cash deposit)
- inventory_unit.status set to "on_trial"
- Customer takes instrument home
- Daily job checks for overdue trials — flags on dashboard, notifies staff
- 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
- 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