{"openapi":"3.1.0","info":{"title":"Nexus366 API","version":"1.0.0","description":"Nexus366 REST API for jewellery retail CRM — Customers, Transactions, Products, Employees, Stores, Gift Cards, Coupons, Vouchers, Loyalty Points, and Customer Wallet.\n\n## Authentication\nAll endpoints require **two** headers:\n- `Authorization: Bearer <API_KEY>` — your tenant API key\n- `X-Tenant-ID: <TENANT_UUID>` — your tenant UUID\n\nMissing or invalid credentials return **401 Unauthorized**.\n\n## Pagination\nList endpoints accept `limit` (1–100, default 50) and `offset` (default 0) query parameters. Responses include a `pagination` object with `limit`, `offset`, and `returned` (records in this page).\n\n## Upsert Pattern\nSeveral endpoints (Customers, Transactions) support upsert via an external ID field (`external_customer_id`, `external_transaction_id`). If the ID matches an existing record, the call updates it; otherwise it creates a new one.\n\n## ERP / POS Integration\nAll resources that can be created from ERP/POS systems support an `external_*_code` field that lets you reference internal Nexus366 UUIDs without knowing them in advance. These codes are resolved server-side before persisting."},"servers":[{"url":"/","description":"Current origin (same-host)"}],"tags":[{"name":"Stores","description":"Physical store locations. Required before creating employees or transactions. Each store has an optional `external_store_code` for ERP integration."},{"name":"Customers","description":"Customer profiles. Supports create, upsert (via `external_customer_id`), and full-text search. Customers are auto-created by the Transaction API when phone/email is provided."},{"name":"Employees","description":"Store staff. Required for employee-level sales tracking. Each employee has a unique `external_employee_code` per tenant."},{"name":"Products","description":"Product catalogue. Supports bulk upsert from ERP/POS with base64 image upload and optional AI embedding generation."},{"name":"Transactions","description":"Sales, returns, exchanges, buy-backs and URD transactions. Line items are stored in the `transaction_items` child table. Supports upsert via `external_transaction_id`. Automatically creates customer walk-in visit records for Invoice and Return types."},{"name":"Gift Cards","description":"Issue and redeem monetary gift cards. Codes are hashed server-side; the plaintext code is returned only at issuance."},{"name":"Coupons","description":"Percentage or fixed-amount discount coupons with usage limits and validity windows."},{"name":"Vouchers","description":"Redeemable value vouchers (percentage or fixed amount). Similar to coupons but support a validate-then-redeem flow."},{"name":"Loyalty","description":"Loyalty points balance and OTP-based redemption flow. Points are awarded by the platform rules engine; redemption requires a 6-digit OTP delivered to the customer."},{"name":"Customer Wallet","description":"Per-customer multi-bucket wallet for sale returns, exchange credits, and scheme maturity amounts. Read-only via public API; mutations happen via the Transaction API `wallet_bucket` field."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"Pass your API key as a Bearer token: `Authorization: Bearer <API_KEY>`"},"tenantId":{"type":"apiKey","in":"header","name":"X-Tenant-ID","description":"Your tenant UUID: `X-Tenant-ID: <TENANT_UUID>`. Required on every request."}},"schemas":{"Error":{"type":"object","description":"Standard error response.","properties":{"error":{"type":"string","description":"Human-readable error message or Zod validation error object serialised as string."}}},"Pagination":{"type":"object","description":"Pagination metadata returned on list endpoints.","properties":{"limit":{"type":"integer","description":"Page size requested."},"offset":{"type":"integer","description":"Number of records skipped."},"returned":{"type":"integer","description":"Number of records returned in this page."}}},"Store":{"type":"object","description":"A physical store / branch belonging to the tenant.","properties":{"id":{"type":"string","format":"uuid","description":"Store UUID (internal primary key)."},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Store display name."},"timezone":{"type":"string","description":"IANA timezone (e.g. Asia/Kolkata)."},"external_store_code":{"type":"string","nullable":true,"description":"ERP/POS code. Unique per tenant. Used to resolve store_id in Transaction, Customer and Employee APIs."},"location":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"pin_code":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"gst_number":{"type":"string","nullable":true,"description":"GST / tax registration number."},"website":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreateStoreRequest":{"type":"object","description":"Request body to create a store. `name` and `timezone` are required.","required":["name","timezone"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"REQUIRED – Store display name."},"timezone":{"type":"string","minLength":1,"maxLength":100,"description":"REQUIRED – Valid IANA timezone (e.g. Asia/Kolkata, America/New_York). Validated server-side via Intl.supportedValuesOf."},"external_store_code":{"type":"string","maxLength":255,"nullable":true,"description":"OPTIONAL – ERP/POS store code. Must be unique per tenant. Used to resolve store_id without knowing the UUID."},"location":{"type":"string","maxLength":1000,"nullable":true,"description":"OPTIONAL – Free-text location description (e.g. floor/mall name)."},"address":{"type":"string","maxLength":1000,"nullable":true,"description":"OPTIONAL – Full postal address."},"city":{"type":"string","maxLength":100,"nullable":true},"pin_code":{"type":"string","maxLength":20,"nullable":true},"phone":{"type":"string","maxLength":30,"nullable":true},"email":{"type":"string","format":"email","nullable":true},"gst_number":{"type":"string","maxLength":50,"nullable":true},"website":{"type":"string","maxLength":255,"nullable":true}}},"UpdateStoreRequest":{"type":"object","description":"Request body to update a store. All fields are optional; only sent fields are updated (PATCH semantics). Setting a field to null clears it.","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"timezone":{"type":"string","minLength":1,"maxLength":100,"description":"Valid IANA timezone. Validated server-side."},"external_store_code":{"type":"string","maxLength":255,"nullable":true,"description":"Set null to clear. Must be unique per tenant."},"location":{"type":"string","maxLength":1000,"nullable":true},"address":{"type":"string","maxLength":1000,"nullable":true},"city":{"type":"string","maxLength":100,"nullable":true},"pin_code":{"type":"string","maxLength":20,"nullable":true},"phone":{"type":"string","maxLength":30,"nullable":true},"email":{"type":"string","format":"email","nullable":true},"gst_number":{"type":"string","maxLength":50,"nullable":true},"website":{"type":"string","maxLength":255,"nullable":true}}},"Customer":{"type":"object","description":"A customer profile. JSON-serialised fields (metadata, additional_emails, additional_phone_numbers, communication_preferences) are returned as strings.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"phone_number":{"type":"string","nullable":true},"customer_status":{"type":"string","enum":["active","inactive","blocked"]},"customer_type":{"type":"string","nullable":true,"description":"e.g. VIP, Regular, New, Premium"},"lead_source":{"type":"string","nullable":true},"external_customer_id":{"type":"string","nullable":true,"description":"ERP/POS customer ID. Used for upsert."},"referral_code":{"type":"string","nullable":true,"description":"Auto-generated REF+8 chars if not provided."},"store_id":{"type":"string","format":"uuid","nullable":true,"description":"Preferred/home store UUID."},"date_of_birth":{"type":"string","format":"date","nullable":true,"description":"YYYY-MM-DD"},"address":{"type":"string","nullable":true},"avatar_url":{"type":"string","format":"uri","nullable":true},"metadata":{"type":"string","nullable":true,"description":"JSON string. Contains anniversary_date and other custom fields."},"additional_emails":{"type":"string","nullable":true,"description":"JSON array string of extra email addresses."},"additional_phone_numbers":{"type":"string","nullable":true,"description":"JSON array string of extra phone numbers."},"communication_preferences":{"type":"string","nullable":true,"description":"JSON object string. Example: {\"sms\": true, \"email\": true, \"promotions\": false}"},"country_id":{"type":"string","format":"uuid","nullable":true},"state_id":{"type":"string","format":"uuid","nullable":true},"city":{"type":"string","nullable":true},"area":{"type":"string","nullable":true},"zip_code":{"type":"string","nullable":true},"timezone":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"UpsertCustomerRequest":{"type":"object","description":"Create or update a customer.\n\n**Upsert behaviour**: If `external_customer_id` is provided and matches an existing customer, only the explicitly provided fields are updated (PATCH semantics on the matched record). If no match is found, a new customer is created — in this case `name` is required.\n\n**Store resolution**: Provide `store_id` (UUID) or `external_store_code`. `store_id` takes priority.","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"REQUIRED for new customers. Ignored on update if name not provided in body."},"external_customer_id":{"type":"string","maxLength":255,"description":"OPTIONAL – ERP/POS customer ID. When provided, triggers upsert lookup. Unique per tenant."},"email":{"type":"string","format":"email","nullable":true,"description":"OPTIONAL – Lowercased before storage."},"phone_number":{"type":"string","minLength":6,"maxLength":20,"nullable":true,"description":"OPTIONAL – Unique per tenant."},"store_id":{"type":"string","format":"uuid","nullable":true,"description":"OPTIONAL – Preferred/home store UUID. Validated against tenant stores."},"external_store_code":{"type":"string","maxLength":255,"nullable":true,"description":"OPTIONAL – Resolved to store_id when store_id not provided."},"lead_source":{"type":"string","maxLength":100,"nullable":true,"description":"OPTIONAL – e.g. Walk-in, Referral, Campaign."},"referral_code":{"type":"string","maxLength":50,"nullable":true,"description":"OPTIONAL – Auto-generated if omitted on create."},"date_of_birth":{"type":"string","format":"date","nullable":true,"description":"OPTIONAL – YYYY-MM-DD"},"anniversary_date":{"type":"string","format":"date","nullable":true,"description":"OPTIONAL – YYYY-MM-DD. Stored in customer metadata under anniversary_date key."},"address":{"type":"string","maxLength":2000,"nullable":true},"avatar_url":{"type":"string","format":"uri","nullable":true},"metadata":{"type":"object","additionalProperties":true,"nullable":true,"description":"OPTIONAL – Arbitrary key-value data merged with existing metadata on update."},"additional_emails":{"type":"array","items":{"type":"string","format":"email"},"nullable":true},"additional_phone_numbers":{"type":"array","items":{"type":"string","maxLength":20},"nullable":true},"customer_type":{"type":"string","maxLength":50,"nullable":true,"description":"e.g. VIP, Regular, New, Premium"},"customer_status":{"type":"string","enum":["active","inactive","blocked"],"nullable":true,"description":"Default: active"},"communication_preferences":{"type":"object","additionalProperties":{"type":"boolean"},"nullable":true,"description":"e.g. {\"sms\": true, \"email\": true, \"promotions\": false}"},"country_id":{"type":"string","format":"uuid","nullable":true},"state_id":{"type":"string","format":"uuid","nullable":true},"city":{"type":"string","maxLength":255,"nullable":true},"area":{"type":"string","maxLength":100,"nullable":true},"zip_code":{"type":"string","maxLength":20,"nullable":true},"timezone":{"type":"string","maxLength":50,"nullable":true,"description":"IANA timezone (e.g. Asia/Kolkata)"}}},"Employee":{"type":"object","description":"A store employee / salesperson.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_employee_code":{"type":"string","description":"Unique ERP/POS employee code per tenant. Used in Transaction API to assign salesperson."},"store_id":{"type":"string","format":"uuid","description":"Store the employee belongs to."},"user_id":{"type":"string","format":"uuid","nullable":true,"description":"Linked Nexus366 user account (optional)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreateEmployeeRequest":{"type":"object","description":"Create a new employee. Requires `name`, `external_employee_code`, and one of `store_id` / `external_store_code`.","required":["name","external_employee_code"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"REQUIRED – Full name of the employee."},"external_employee_code":{"type":"string","minLength":1,"maxLength":255,"description":"REQUIRED – Unique ERP/POS employee code per tenant. Used by Transaction API via external_employee_code field."},"store_id":{"type":"string","format":"uuid","description":"REQUIRED (if external_store_code not provided) – Store UUID."},"external_store_code":{"type":"string","maxLength":255,"description":"REQUIRED (if store_id not provided) – Resolved to store_id before insert."},"user_id":{"type":"string","format":"uuid","nullable":true,"description":"OPTIONAL – Link to a Nexus366 user account."}}},"Product":{"type":"object","description":"Product for the upsert request. At least one of `external_product_id`, `sku`, or `code` is required for upsert lookup.","properties":{"external_product_id":{"type":"string","minLength":1,"description":"OPTIONAL (required if sku/code absent) – ERP/POS product ID string."},"code":{"type":"string","minLength":1,"description":"OPTIONAL (required if external_product_id/sku absent) – Product code. Used as lookup key in Transaction API item_code."},"sku":{"type":"string","minLength":1,"description":"OPTIONAL (required if external_product_id/code absent) – Stock-keeping unit."},"name":{"type":"string","minLength":1,"description":"OPTIONAL – Product display name."},"description":{"type":"string","nullable":true},"price":{"type":"number","minimum":0},"image_url":{"type":"string","format":"uri","nullable":true,"description":"OPTIONAL – Pre-existing hosted image URL."},"image":{"type":"string","description":"OPTIONAL – Base64-encoded image (JPEG/PNG). Uploaded to storage → image_url. AI embedding generated if tenant has vector_search_enabled = true."},"category":{"type":"string","nullable":true},"subcategory":{"type":"string","nullable":true},"collection":{"type":"string","nullable":true},"brand":{"type":"string","nullable":true},"item_group":{"type":"string","nullable":true},"net_weight":{"type":"number","minimum":0,"nullable":true,"description":"Net metal weight in grams."},"diamond_pieces":{"type":"integer","minimum":0,"nullable":true},"diamond_weight":{"type":"number","minimum":0,"nullable":true},"cs_pieces":{"type":"integer","minimum":0,"nullable":true,"description":"Colour stone piece count."},"cs_weight":{"type":"number","minimum":0,"nullable":true},"stone_pieces":{"type":"integer","minimum":0,"nullable":true},"stone_weight":{"type":"number","minimum":0,"nullable":true},"other_pieces":{"type":"integer","minimum":0,"nullable":true},"other_weight":{"type":"number","minimum":0,"nullable":true},"similar_items":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Array of product codes/IDs considered similar (used for recommendations)."},"metadata":{"type":"object","nullable":true,"description":"Arbitrary additional attributes (metal, metal_type, style, etc.)."}}},"ProductsUpsertRequest":{"type":"object","description":"Bulk product upsert. Each product must have at least one of `external_product_id`, `sku`, or `code`. Matched by those fields in priority order; unmatched products are inserted.","required":["products"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"},"minItems":1,"description":"REQUIRED – Array of products to upsert. Min 1 item."}}},"WalletBucket":{"type":"object","description":"Wallet credit to apply after a transaction. Requires a linked customer (customer_id or auto-created).","required":["bucket_type","amount"],"properties":{"bucket_type":{"type":"string","enum":["sale_return","exchange","scheme_maturity"],"description":"REQUIRED – Type of wallet credit."},"amount":{"type":"number","minimum":0.01,"description":"REQUIRED – Amount to credit."},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"OPTIONAL – ISO 4217 currency code (e.g. INR, USD). Default: tenant currency."},"origin_reference_id":{"type":"string","format":"uuid","description":"OPTIONAL – Source transaction/return UUID for traceability."},"origin_reference_type":{"type":"string","maxLength":50,"description":"OPTIONAL – Type of the origin reference (e.g. 'transaction')."},"expires_at":{"type":"string","format":"date","description":"OPTIONAL – Expiry date YYYY-MM-DD."},"maturity_date":{"type":"string","format":"date","description":"OPTIONAL – Maturity date YYYY-MM-DD (for scheme_maturity type)."},"notes":{"type":"string","maxLength":500,"description":"OPTIONAL – Internal notes."}}},"TransactionItem":{"type":"object","description":"A line item in a transaction. Stored in the transaction_items child table. One of product_id, item_code, or external_product_id is REQUIRED for product lookup. If no matching product is found the API will auto-create one using name, price, image_url, brand, item_group, net_weight, diamond_pieces, diamond_weight, color_stone_pieces/cs_pieces, color_stone_weight/cs_weight, stone_pieces, stone_weight, other_pieces, other_weight, category, subcategory, collection.","required":[],"properties":{"product_id":{"type":"string","format":"uuid","description":"OPTIONAL – UUID of an existing product. Takes priority over item_code and external_product_id."},"item_code":{"type":"string","minLength":1,"maxLength":100,"description":"OPTIONAL (required if product_id and external_product_id are absent) – Maps to products.code. Used for lookup and as products.code+sku when auto-creating a product. Do NOT use for display-only SKU — use sku field for that."},"external_product_id":{"type":["integer","string"],"description":"OPTIONAL (required if product_id and item_code are absent) – ERP/POS product ID (bigint). Stored as integer on transaction_items; stringified for products table lookup. Can be sent as number or numeric string."},"sku":{"type":"string","maxLength":100,"description":"OPTIONAL – Display-only SKU for receipt/invoice line. NOT used for product lookup."},"name":{"type":"string","maxLength":255,"description":"OPTIONAL – Product name. Required when auto-creating a new product."},"price":{"type":"number","minimum":0,"description":"OPTIONAL – Unit price. Required when auto-creating a product."},"amount":{"type":"number","minimum":0,"description":"OPTIONAL – Line total (price × pieces). Calculated by caller; stored as-is."},"pieces":{"type":"integer","minimum":1,"default":1,"description":"OPTIONAL – Piece count (replaces quantity for jewellery). Default: 1."},"weight":{"type":"number","minimum":0,"description":"OPTIONAL – Gross weight in grams."},"net_weight":{"type":"number","minimum":0,"description":"OPTIONAL – Net metal weight in grams (after deducting stone weight)."},"diamond_pieces":{"type":"integer","minimum":0,"description":"OPTIONAL – Number of diamonds."},"diamond_weight":{"type":"number","minimum":0,"description":"OPTIONAL – Diamond weight in carats."},"diamond_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Diamond value component of the line total."},"color_stone_pieces":{"type":"integer","minimum":0,"description":"OPTIONAL – Number of colour stones."},"color_stone_weight":{"type":"number","minimum":0,"description":"OPTIONAL – Colour stone weight."},"color_stone_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Colour stone value component."},"stone_pieces":{"type":"integer","minimum":0,"description":"OPTIONAL – Number of other precious stones."},"stone_weight":{"type":"number","minimum":0,"description":"OPTIONAL – Other stone weight."},"stone_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Other stone value component."},"other_pieces":{"type":"integer","minimum":0,"description":"OPTIONAL – Miscellaneous component piece count."},"other_weight":{"type":"number","minimum":0,"description":"OPTIONAL – Miscellaneous component weight."},"other_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Miscellaneous component value."},"labour":{"type":"number","minimum":0,"description":"OPTIONAL – Making/labour charges for this line."},"tax_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Tax applied to this line item (e.g. GST/VAT)."},"discount":{"type":"number","minimum":0,"description":"OPTIONAL – Discount applied to this line."},"item_size":{"type":"string","maxLength":50,"description":"OPTIONAL – Size descriptor, e.g. ring size '16' or 'M'."},"category":{"type":"string","maxLength":255,"description":"OPTIONAL – Product category. Used when auto-creating product (e.g. Rings, Necklaces)."},"brand":{"type":"string","maxLength":255,"description":"OPTIONAL – Product brand. Used when auto-creating product."},"item_group":{"type":"string","maxLength":255,"description":"OPTIONAL – Item group/family. Used when auto-creating product."},"subcategory":{"type":"string","maxLength":255,"description":"OPTIONAL – Product sub-category."},"collection":{"type":"string","maxLength":255,"description":"OPTIONAL – Collection/range name."},"image":{"type":"string","description":"OPTIONAL – Base64-encoded product image (data:image/jpeg;base64,... or raw base64). Validated, uploaded to storage, and URL stored in image_url. Embedding generated only when auto-creating product and tenant has vector_search_enabled."},"image_url":{"type":"string","format":"uri","description":"OPTIONAL – Direct URL of product image. Use when image is already hosted. If both image (base64) and image_url are provided, base64 is processed and image_url is overwritten."},"external_employee_code":{"type":"string","minLength":1,"maxLength":255,"description":"OPTIONAL – ERP/POS employee code for this specific line. Overrides transaction-level employee. Resolved to employee_id before insert."}}},"TransactionItemResponse":{"type":"object","description":"A transaction_items row returned in GET /api/v1/transactions responses. Decimal values are strings (Drizzle ORM convention).","properties":{"id":{"type":"string","format":"uuid"},"transaction_id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"product_id":{"type":"string","format":"uuid","description":"Always populated."},"employee_id":{"type":"string","format":"uuid","nullable":true},"line_number":{"type":"integer","description":"1-based sort order within the transaction."},"name":{"type":"string","nullable":true},"item_code":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"external_product_id":{"type":"integer","nullable":true},"price":{"type":"string","nullable":true},"amount":{"type":"string","nullable":true},"image_url":{"type":"string","nullable":true},"pieces":{"type":"integer"},"weight":{"type":"string","nullable":true},"net_weight":{"type":"string","nullable":true},"diamond_pieces":{"type":"integer","nullable":true},"diamond_weight":{"type":"string","nullable":true},"diamond_amount":{"type":"string","nullable":true},"color_stone_pieces":{"type":"integer","nullable":true},"color_stone_weight":{"type":"string","nullable":true},"color_stone_amount":{"type":"string","nullable":true},"stone_pieces":{"type":"integer","nullable":true},"stone_weight":{"type":"string","nullable":true},"stone_amount":{"type":"string","nullable":true},"other_pieces":{"type":"integer","nullable":true},"other_weight":{"type":"string","nullable":true},"other_amount":{"type":"string","nullable":true},"labour":{"type":"string","nullable":true},"tax_amount":{"type":"string","nullable":true},"discount":{"type":"string","nullable":true},"item_size":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TransactionRequest":{"type":"object","description":"Request body for creating or updating a transaction.\n\n**Upsert**: Matched by (external_transaction_id, document_type). Same external_transaction_id can exist for different document_types (e.g. Invoice and Return) as they come from different ERP tables. Update only when both match; otherwise create. Returns 200 on update, 201 on create.\n\n**Store resolution**: Provide `store_id` or `external_store_code`. Both absent → 400.\n\n**Employee resolution**: `external_employee_code` at transaction level applies to all items without their own employee code.\n\n**Product resolution per item** (priority order): `product_id` → `item_code` → `external_product_id`. If none match, auto-creates product using `name`/`price`/`category`/etc.\n\n**Walk-in visit**: Invoice and Return transactions auto-create a customer walk-in record. 3-hour deduplication window per customer per day.","required":["document_no","items","total_amount"],"properties":{"document_no":{"type":"string","minLength":1,"maxLength":255,"description":"REQUIRED – Voucher/document number from source system (e.g. INV-2024-00123). Shown in customer transaction grid."},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/TransactionItem"},"description":"REQUIRED – Line items. Each must include product_id, item_code, or external_product_id."},"total_amount":{"type":"number","minimum":0,"description":"REQUIRED – Grand total of the transaction."},"store_id":{"type":"string","format":"uuid","description":"REQUIRED (if external_store_code absent) – Store UUID."},"external_store_code":{"type":"string","maxLength":255,"description":"REQUIRED (if store_id absent) – Resolved to store_id before persist."},"external_transaction_id":{"type":"string","maxLength":255,"description":"OPTIONAL – Source system transaction ID. Triggers upsert if matched. Unique per tenant."},"external_id":{"type":"string","maxLength":255,"description":"OPTIONAL – Legacy alias for external_transaction_id."},"document_type":{"type":"string","enum":["Invoice","Return","Exchange","BuyBack","URD"],"default":"Invoice","description":"OPTIONAL – Default: Invoice."},"status":{"type":"string","enum":["pending","completed","cancelled","refunded"],"default":"completed","description":"OPTIONAL – Default: completed."},"transaction_date":{"type":"string","format":"date-time","description":"OPTIONAL – ISO 8601 datetime for backdating. Defaults to current server time."},"tax_amount":{"type":"number","minimum":0,"nullable":true,"description":"OPTIONAL – Header-level tax total. Default: 0."},"discount_amount":{"type":"number","minimum":0,"nullable":true,"description":"OPTIONAL – Header-level discount total. Default: 0."},"receipt_details":{"type":"object","nullable":true,"description":"OPTIONAL – Flexible JSON for tax breakdown. Example: {\"tax_lines\": [{\"label\": \"GST 3%\", \"rate\": 0.03, \"amount\": 1500}]}"},"external_employee_code":{"type":"string","minLength":1,"maxLength":255,"nullable":true,"description":"OPTIONAL – Transaction-level employee code. Resolved to employee_id."},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"OPTIONAL – Existing customer UUID. If set, phone_number/email are ignored for lookup."},"phone_number":{"type":"string","minLength":6,"maxLength":20,"nullable":true,"description":"OPTIONAL – Find or create customer by phone (unique per tenant). Priority over email."},"email":{"type":"string","format":"email","nullable":true,"description":"OPTIONAL – Find or create customer by email when customer_id and phone_number are absent."},"customer_name":{"type":"string","maxLength":255,"nullable":true,"description":"OPTIONAL – Name for auto-created customer. Default: Customer XXXX (last 4 of phone)."},"external_customer_id":{"type":"string","maxLength":255,"nullable":true,"description":"OPTIONAL – ERP customer ID stored on customer record."},"address":{"type":"string","maxLength":2000,"nullable":true,"description":"OPTIONAL – Customer address for auto-created customer."},"lead_source":{"type":"string","maxLength":100,"nullable":true,"description":"OPTIONAL – How the customer was acquired."},"customer_type":{"type":"string","maxLength":50,"nullable":true},"country_id":{"type":"string","format":"uuid","nullable":true},"state_id":{"type":"string","format":"uuid","nullable":true},"city":{"type":"string","maxLength":255,"nullable":true},"area":{"type":"string","maxLength":100,"nullable":true},"zip_code":{"type":"string","maxLength":20,"nullable":true},"timezone":{"type":"string","maxLength":50,"nullable":true},"date_of_birth":{"type":"string","format":"date","nullable":true,"description":"OPTIONAL – Customer DOB YYYY-MM-DD."},"birth_date":{"type":"string","format":"date","nullable":true,"description":"OPTIONAL – Alias for date_of_birth (some ERPs use birth_date)."},"anniversary_date":{"type":"string","format":"date","nullable":true,"description":"OPTIONAL – Customer anniversary YYYY-MM-DD."},"wallet_bucket":{"$ref":"#/components/schemas/WalletBucket","description":"OPTIONAL – Wallet credit after transaction. Requires a resolved customer."},"campaign_id":{"type":"string","format":"uuid","nullable":true,"description":"OPTIONAL – Campaign UUID for attribution."},"event_id":{"type":"string","format":"uuid","nullable":true,"description":"OPTIONAL – Marketing event UUID for attribution."},"utm_source":{"type":"string","maxLength":255,"nullable":true},"utm_medium":{"type":"string","maxLength":255,"nullable":true},"utm_campaign":{"type":"string","maxLength":255,"nullable":true},"attribution_window":{"type":"integer","minimum":1,"maximum":365,"default":30,"description":"OPTIONAL – Attribution window in days. Default: 30."},"conversion_type":{"type":"string","enum":["campaign","event","lead_form","social_post","direct"],"nullable":true}}},"Transaction":{"type":"object","description":"Transaction record returned by GET /api/v1/transactions. Decimal amounts are strings. items is populated from the transaction_items child table.","properties":{"id":{"type":"string","format":"uuid"},"store_id":{"type":"string","format":"uuid"},"customer_id":{"type":"string","format":"uuid","nullable":true},"employee_id":{"type":"string","format":"uuid","nullable":true},"document_no":{"type":"string","description":"Voucher/document number from source system."},"document_type":{"type":"string","enum":["Invoice","Return","Exchange","BuyBack","URD"]},"status":{"type":"string","enum":["pending","completed","cancelled","refunded"]},"total_amount":{"type":"string"},"tax_amount":{"type":"string"},"discount_amount":{"type":"string"},"receipt_details":{"type":"object","nullable":true},"items":{"type":"array","items":{"$ref":"#/components/schemas/TransactionItemResponse"},"description":"Line items ordered by line_number. Populated on GET; not returned in POST/upsert response (re-fetch if needed)."},"external_transaction_id":{"type":"string","nullable":true},"external_id":{"type":"string","nullable":true},"campaign_id":{"type":"string","format":"uuid","nullable":true},"event_id":{"type":"string","format":"uuid","nullable":true},"utm_source":{"type":"string","nullable":true},"utm_medium":{"type":"string","nullable":true},"utm_campaign":{"type":"string","nullable":true},"transaction_date":{"type":"string","format":"date-time","nullable":true,"description":"Business date of transaction (for backdating)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"GiftCard":{"type":"object","description":"A gift card record. The `code` is returned only at issuance; subsequent access uses the UUID.","properties":{"id":{"type":"string","format":"uuid"},"customer_id":{"type":"string","format":"uuid","nullable":true},"initial_amount":{"type":"string","description":"Original issued amount as decimal string."},"balance":{"type":"string","description":"Current remaining balance as decimal string."},"currency":{"type":"string"},"status":{"type":"string","enum":["active","redeemed","expired","cancelled"]},"issued_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","nullable":true},"redeemed_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"GiftCardTransaction":{"type":"object","description":"A transaction event on a gift card (issued, redeemed, etc.).","properties":{"id":{"type":"string","format":"uuid"},"gift_card_id":{"type":"string","format":"uuid"},"transaction_type":{"type":"string","enum":["issued","redeemed","adjusted","cancelled"]},"amount":{"type":"string"},"balance_after":{"type":"string"},"transaction_reference":{"type":"string","nullable":true,"description":"Linked transaction UUID if redeemed during a purchase."},"notes":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"IssueGiftCardRequest":{"type":"object","description":"Issue a new gift card. Returns the plaintext code (store securely; not recoverable later).","required":["initial_amount"],"properties":{"initial_amount":{"type":"number","minimum":1,"description":"REQUIRED – Initial monetary value."},"customer_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link to an existing customer."},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"USD","description":"OPTIONAL – ISO 4217 currency code. Default: USD."},"recipient_name":{"type":"string","minLength":1,"maxLength":255,"description":"OPTIONAL – Recipient name for the gift note."},"recipient_email":{"type":"string","format":"email","description":"OPTIONAL – Recipient email."},"message":{"type":"string","description":"OPTIONAL – Personal message on the gift card."},"valid_until":{"type":"string","format":"date","description":"OPTIONAL – Expiry date (YYYY-MM-DD)."}}},"RedeemGiftCardRequest":{"type":"object","description":"Redeem an amount from a gift card. Validates code via argon2 hash comparison.","required":["code","amount"],"properties":{"code":{"type":"string","minLength":12,"maxLength":19,"description":"REQUIRED – Gift card code (e.g. ABCD-1234-EFGH-5678). Dashes are optional."},"amount":{"type":"number","minimum":1,"description":"REQUIRED – Amount to deduct. Must be ≤ current balance."},"transaction_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link to the sale transaction UUID for traceability."},"notes":{"type":"string","description":"OPTIONAL – Redemption notes."}}},"RedeemCouponRequest":{"type":"object","description":"Redeem a discount coupon. Returns the computed discount amount. Coupon usage count is incremented atomically.","required":["code","purchase_amount"],"properties":{"code":{"type":"string","minLength":1,"maxLength":50,"description":"REQUIRED – Coupon code (case-insensitive; converted to uppercase)."},"purchase_amount":{"type":"number","minimum":0,"description":"REQUIRED – Basket total before discount. Used to compute percentage discounts and validate min_purchase_amount."},"customer_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link coupon usage to a customer."},"transaction_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link to a transaction UUID."}}},"RedeemVoucherRequest":{"type":"object","description":"Redeem a voucher. Atomically decrements usage count. Returns computed discount amount.","required":["code","purchase_amount"],"properties":{"code":{"type":"string","minLength":1,"maxLength":50,"description":"REQUIRED – Voucher code (case-insensitive)."},"purchase_amount":{"type":"number","minimum":0,"description":"REQUIRED – Basket total before discount."},"customer_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link usage to a customer."},"transaction_id":{"type":"string","format":"uuid","description":"OPTIONAL – Link to a transaction UUID."}}},"ValidateVoucherRequest":{"type":"object","description":"Validate a voucher without redeeming it. Returns discount details and computed amount. Safe to call repeatedly.","required":["code"],"properties":{"code":{"type":"string","minLength":1,"maxLength":50,"description":"REQUIRED – Voucher code."},"purchase_amount":{"type":"number","minimum":0,"description":"OPTIONAL – Basket total. Required for percentage discount calculation and min_purchase_amount validation."}}},"GenerateOtpRequest":{"type":"object","description":"Generate a 6-digit OTP for loyalty points redemption. OTP is delivered to the customer out-of-band (SMS/email). Validates customer exists and has sufficient balance.","required":["customer_id","points"],"properties":{"customer_id":{"type":"string","format":"uuid","description":"REQUIRED – Customer UUID."},"points":{"type":"integer","minimum":1,"description":"REQUIRED – Points to redeem. Must be ≤ customer's current balance."}}},"VerifyOtpRequest":{"type":"object","description":"Verify the OTP and execute the loyalty points redemption. OTP is single-use and time-limited.","required":["customer_id","otp"],"properties":{"customer_id":{"type":"string","format":"uuid","description":"REQUIRED – Customer UUID."},"otp":{"type":"string","minLength":6,"maxLength":6,"description":"REQUIRED – 6-digit numeric OTP received by the customer."}}},"WalletSummary":{"type":"object","description":"Customer wallet summary across all buckets.","properties":{"total_balance":{"type":"number","description":"Sum of all active bucket balances."},"buckets":{"type":"array","description":"Individual wallet buckets.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bucket_type":{"type":"string","enum":["sale_return","exchange","scheme_maturity"]},"balance":{"type":"number"},"currency":{"type":"string"},"expires_at":{"type":"string","format":"date-time","nullable":true},"maturity_date":{"type":"string","format":"date-time","nullable":true},"origin_reference_id":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}},"WalletTransaction":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bucket_id":{"type":"string","format":"uuid"},"transaction_type":{"type":"string","enum":["credit","debit"]},"amount":{"type":"number"},"balance_after":{"type":"number"},"notes":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}},"security":[{"bearerAuth":[]},{"tenantId":[]}],"paths":{"/api/v1/stores":{"get":{"summary":"List stores","description":"Returns a paginated list of stores for the authenticated tenant. Supports search by store name.","tags":["Stores"],"parameters":[{"name":"limit","in":"query","description":"Max records (1–100). Default: 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","description":"Records to skip. Default: 0.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"search","in":"query","description":"Full-text search on store name (ILIKE).","schema":{"type":"string","maxLength":100}}],"responses":{"200":{"description":"List of stores with pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Store"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create store","description":"Create a new store for the tenant. `name` and `timezone` are required. `external_store_code` must be unique per tenant.","tags":["Stores"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStoreRequest"},"example":{"name":"Flagship – Zaveri Bazaar","timezone":"Asia/Kolkata","external_store_code":"STORE-MUM-01","address":"Zaveri Bazaar, Mumbai","city":"Mumbai","pin_code":"400002","phone":"+91 22 2345 6789","gst_number":"27AABCU9603R1ZX"}}}},"responses":{"201":{"description":"Store created.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Store"}}}}}},"400":{"description":"Validation error (e.g. invalid timezone).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Store with external_store_code already exists for this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/stores/{id}":{"get":{"summary":"Get store by ID","description":"Retrieve a single store by its UUID. Returns 404 if not found or not owned by the tenant.","tags":["Stores"],"parameters":[{"name":"id","in":"path","required":true,"description":"Store UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Store record.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Store"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Store not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update store","description":"Partially update a store. Only provided fields are changed (PATCH semantics). Send `null` for a field to clear it.","tags":["Stores"],"parameters":[{"name":"id","in":"path","required":true,"description":"Store UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStoreRequest"},"example":{"city":"Surat","phone":"+91 261 234 5678","external_store_code":"STORE-SRT-02"}}}},"responses":{"200":{"description":"Store updated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Store"}}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Store not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"external_store_code already in use by another store.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers":{"get":{"summary":"List customers","description":"Paginated customer list with optional full-text search across name, email, and phone_number (ILIKE).","tags":["Customers"],"parameters":[{"name":"limit","in":"query","description":"Max records (1–100). Default: 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","description":"Records to skip. Default: 0.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"search","in":"query","description":"Search by name, email, or phone_number.","schema":{"type":"string","maxLength":100}}],"responses":{"200":{"description":"Customer list.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create or upsert customer","description":"Create a new customer or update an existing one.\n\n**Upsert**: If `external_customer_id` matches an existing customer, only the explicitly provided fields are updated (PATCH semantics). Returns the updated record without HTTP 201.\n\n**Create**: If no match, creates a new customer. `name` is required in this case.\n\n**Store resolution**: Supply `store_id` (UUID) or `external_store_code`. `store_id` takes priority.","tags":["Customers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomerRequest"},"examples":{"create_new":{"summary":"Create new customer","value":{"name":"Priya Sharma","phone_number":"+919876543210","email":"priya@example.com","date_of_birth":"1992-03-15","anniversary_date":"2018-11-20","customer_type":"VIP","lead_source":"Walk-in","store_id":"550e8400-e29b-41d4-a716-446655440000","city":"Mumbai","timezone":"Asia/Kolkata","communication_preferences":{"sms":true,"email":true,"promotions":true}}},"upsert_by_external_id":{"summary":"Upsert by external_customer_id","value":{"external_customer_id":"ERP-CUST-5001","name":"Rahul Mehta","phone_number":"+919812345678","external_store_code":"STORE-MUM-01","customer_type":"Regular"}}}}}},"responses":{"200":{"description":"Customer updated (upsert via external_customer_id).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Customer"}}}}}},"201":{"description":"Customer created.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Customer"}}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"name_required":{"summary":"Name required for new customer","value":{"error":"name is required for new customers"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/employees":{"get":{"summary":"List employees","description":"Paginated employee list. Optionally filter by store_id.","tags":["Employees"],"parameters":[{"name":"limit","in":"query","description":"Max records (1–100). Default: 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","description":"Records to skip. Default: 0.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"store_id","in":"query","description":"Filter by store UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Employee list.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Employee"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create employee","description":"Create a new employee. `external_employee_code` must be unique per tenant. Store is required — supply `store_id` or `external_store_code`.","tags":["Employees"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmployeeRequest"},"examples":{"by_store_id":{"summary":"Create using store UUID","value":{"name":"Anjali Patel","external_employee_code":"EMP-0042","store_id":"550e8400-e29b-41d4-a716-446655440000"}},"by_store_code":{"summary":"Create using external_store_code","value":{"name":"Suresh Kumar","external_employee_code":"EMP-0043","external_store_code":"STORE-MUM-01"}}}}}},"responses":{"201":{"description":"Employee created.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Employee"}}}}}},"400":{"description":"Validation error or store not resolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"no_store":{"summary":"Store required","value":{"error":"store_id or external_store_code is required"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"store_id is valid UUID but does not belong to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Employee with this external_employee_code already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products":{"post":{"summary":"Bulk upsert products","description":"Create or update a batch of products. Each product must have at least one of `external_product_id`, `sku`, or `code` for upsert lookup — matched in that priority order.\n\nIf `image` (base64) is provided, it is uploaded to storage and `image_url` is set. If the tenant has `vector_search_enabled = true`, an AI embedding is also generated for semantic search.","tags":["Products"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductsUpsertRequest"},"example":{"products":[{"code":"RNG-22KT-001","external_product_id":"78452310","sku":"SKU-RNG-001","name":"22KT Plain Gold Ring","description":"Hallmarked 22KT plain gold ring","price":45000,"category":"Rings","subcategory":"Plain","item_group":"Gold","collection":"Everyday Classics","brand":"Tanishq","net_weight":8.5,"metadata":{"metal":"Gold","metal_type":"22KT","style":"Plain"}}]}}}},"responses":{"200":{"description":"Products upserted. Returns count of inserted and updated records.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"inserted":{"type":"integer","description":"Number of new products created."},"updated":{"type":"integer","description":"Number of existing products updated."}}}}}},"400":{"description":"Validation error or missing lookup key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products/sync":{"post":{"summary":"Sync products (alias)","description":"Alias for `POST /api/v1/products`. Identical behaviour — bulk upsert from ERP/POS.","tags":["Products"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductsUpsertRequest"}}}},"responses":{"200":{"description":"Products upserted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"inserted":{"type":"integer"},"updated":{"type":"integer"}}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/transactions":{"get":{"summary":"List transactions","description":"Paginated transaction list. Each transaction includes its full line items array from the `transaction_items` child table. Filter by customer, store, document type, or status.","tags":["Transactions"],"parameters":[{"name":"limit","in":"query","description":"Max records (1–100). Default: 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","description":"Records to skip. Default: 0.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"customer_id","in":"query","description":"Filter by customer UUID.","schema":{"type":"string","format":"uuid"}},{"name":"store_id","in":"query","description":"Filter by store UUID.","schema":{"type":"string","format":"uuid"}},{"name":"document_type","in":"query","description":"Filter by document type.","schema":{"type":"string","enum":["Invoice","Return","Exchange","BuyBack","URD"]}},{"name":"status","in":"query","description":"Filter by status.","schema":{"type":"string","enum":["pending","completed","cancelled","refunded"]}}],"responses":{"200":{"description":"Transaction list with pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Transaction"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create or upsert transaction","description":"Creates a new transaction with its line items.\n\n**Upsert**: If `external_transaction_id` matches an existing transaction, updates the header and replaces all line items. Returns **200** on update, **201** on create.\n\n**Store resolution**: Provide `store_id` (UUID) or `external_store_code`. Both absent → 400.\n\n**Employee resolution**: `external_employee_code` at transaction level applied to all items unless overridden per item.\n\n**Product resolution per item** (priority): `product_id` → `item_code` (maps to products.code) → `external_product_id`. If no match, auto-creates product using `name`, `price`, `category`, `item_group`, `subcategory`, `collection`. `sku` is NOT used for lookup.\n\n**Image on auto-create**: `image` (base64) uploaded and embedding generated only when product is newly created AND tenant has `vector_search_enabled = true`.\n\n**Walk-in visit**: Invoice and Return types auto-create a customer walk-in record. 3-hour deduplication window per customer per day.","tags":["Transactions"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionRequest"},"examples":{"invoice_by_item_code":{"summary":"Invoice – lookup by item_code","value":{"document_no":"INV-2024-00123","store_id":"550e8400-e29b-41d4-a716-446655440000","document_type":"Invoice","external_transaction_id":"ERP-TXN-9001","external_employee_code":"EMP-0042","total_amount":55000,"tax_amount":1650,"discount_amount":500,"phone_number":"+919876543210","customer_name":"Priya Sharma","lead_source":"Walk-in","items":[{"item_code":"RNG-22KT-001","name":"22KT Gold Ring","price":45000,"amount":45000,"pieces":1,"weight":8.5,"net_weight":8.2,"labour":2000,"tax_amount":1350,"item_size":"16","category":"Rings","item_group":"Gold","sku":"SKU-RNG-001"},{"item_code":"EAR-22KT-002","name":"22KT Gold Earrings","price":10000,"amount":10000,"pieces":1,"weight":3.2,"diamond_pieces":6,"diamond_weight":0.18,"diamond_amount":2500,"labour":800,"tax_amount":300,"category":"Earrings","collection":"Festive 2024"}],"receipt_details":{"tax_lines":[{"label":"GST 3%","rate":0.03,"amount":1650}]}}},"invoice_by_external_product_id":{"summary":"Invoice – lookup by external_product_id (ERP bigint)","value":{"document_no":"INV-2024-00124","external_store_code":"STORE-MUM-01","document_type":"Invoice","external_transaction_id":"ERP-TXN-9002","external_employee_code":"EMP-0043","total_amount":120000,"customer_id":"550e8400-e29b-41d4-a716-446655440001","items":[{"external_product_id":78452310,"name":"Diamond Necklace","price":120000,"pieces":1,"weight":15.5,"net_weight":12,"diamond_pieces":48,"diamond_weight":1.5,"diamond_amount":60000,"color_stone_pieces":8,"color_stone_weight":0.8,"color_stone_amount":8000,"labour":15000,"tax_amount":3600,"category":"Necklaces","item_group":"Diamond","collection":"Bridal"}]}},"return":{"summary":"Return transaction","value":{"document_no":"RET-2024-00045","store_id":"550e8400-e29b-41d4-a716-446655440000","document_type":"Return","external_transaction_id":"ERP-RET-5001","external_employee_code":"EMP-0042","total_amount":45000,"customer_id":"550e8400-e29b-41d4-a716-446655440001","items":[{"item_code":"RNG-22KT-001","price":45000,"pieces":1,"weight":8.5}]}},"upsert":{"summary":"Upsert existing transaction","description":"If ERP-TXN-9001 already exists, updates header and replaces line items. Returns HTTP 200.","value":{"document_no":"INV-2024-00123","store_id":"550e8400-e29b-41d4-a716-446655440000","external_transaction_id":"ERP-TXN-9001","total_amount":56000,"items":[{"item_code":"RNG-22KT-001","price":46000,"pieces":1,"weight":8.5}]}},"with_wallet_bucket":{"summary":"Invoice + wallet cashback credit","value":{"document_no":"INV-2024-00125","store_id":"550e8400-e29b-41d4-a716-446655440000","document_type":"Invoice","total_amount":10000,"customer_id":"550e8400-e29b-41d4-a716-446655440001","items":[{"item_code":"BANGLE-001","price":10000,"pieces":1}],"wallet_bucket":{"bucket_type":"sale_return","amount":500,"currency":"INR","notes":"Cashback on festive purchase"}}},"auto_create_product":{"summary":"Auto-create product with image","value":{"document_no":"INV-2024-00126","store_id":"550e8400-e29b-41d4-a716-446655440000","total_amount":75000,"items":[{"item_code":"NEW-PLAT-001","name":"Platinum Bangle","price":75000,"pieces":1,"weight":20,"net_weight":19.5,"category":"Bangles","item_group":"Platinum","subcategory":"Plain","collection":"Platinum Essentials","image":"data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."}]}}}}}},"responses":{"200":{"description":"Existing transaction updated (upsert via external_transaction_id).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Transaction"}}}}}},"201":{"description":"Transaction created.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Transaction"}}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing_document_no":{"summary":"document_no missing","value":{"error":"document_no is required"}},"store_not_found":{"summary":"Store not resolved","value":{"error":"store not found for external_store_code: STORE-UNKNOWN"}},"product_missing_name":{"summary":"Product auto-create failed","value":{"error":"Item 1: product not found and name is required to auto-create"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"store_id does not belong to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/gift-cards":{"get":{"summary":"List gift cards","description":"List all gift cards for the tenant. Filter by status or customer.","tags":["Gift Cards"],"parameters":[{"name":"status","in":"query","description":"Filter by gift card status.","schema":{"type":"string","enum":["active","redeemed","expired","cancelled"]}},{"name":"customer_id","in":"query","description":"Filter by customer UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Gift card list.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GiftCard"}}}}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Issue gift card","description":"Issue a new gift card. The plaintext `code` is returned **once** at issuance — store it securely. Subsequent access uses the gift_card_id UUID.","tags":["Gift Cards"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueGiftCardRequest"},"example":{"initial_amount":5000,"currency":"INR","customer_id":"550e8400-e29b-41d4-a716-446655440001","recipient_name":"Priya Sharma","recipient_email":"priya@example.com","message":"Happy Anniversary!","valid_until":"2026-12-31"}}}},"responses":{"201":{"description":"Gift card issued. Returns plaintext code — store securely.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"gift_card_id":{"type":"string","format":"uuid","description":"UUID to reference the card in future API calls."},"code":{"type":"string","description":"Plaintext code (e.g. ABCD-1234-EFGH-5678). Shown only once."},"initial_amount":{"type":"number"},"currency":{"type":"string"},"recipient_name":{"type":"string","nullable":true},"recipient_email":{"type":"string","nullable":true},"message":{"type":"string","nullable":true}}}}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/gift-cards/{id}":{"get":{"summary":"Get gift card with transaction history","description":"Retrieve a gift card by UUID along with its full transaction history (issued, redeemed events).","tags":["Gift Cards"],"parameters":[{"name":"id","in":"path","required":true,"description":"Gift card UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Gift card with transactions.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"gift_card":{"$ref":"#/components/schemas/GiftCard"},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/GiftCardTransaction"}}}}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Gift card not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/gift-cards/balance":{"get":{"summary":"Check gift card balance","description":"Look up balance and status by code. The code is hashed server-side — this endpoint iterates active cards to find a match (argon2 verify).","tags":["Gift Cards"],"parameters":[{"name":"code","in":"query","required":true,"description":"Gift card code (dashes optional).","schema":{"type":"string","minLength":12}}],"responses":{"200":{"description":"Gift card balance.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"gift_card_id":{"type":"string","format":"uuid"},"current_balance":{"type":"number"},"currency":{"type":"string"},"status":{"type":"string","enum":["active","redeemed","expired","cancelled"]},"issued_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","nullable":true}}}}}}}},"400":{"description":"code param missing or invalid gift card code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/gift-cards/redeem":{"post":{"summary":"Redeem gift card","description":"Deduct an amount from a gift card balance. Validates code via argon2 hash. Balance is atomically decremented — if balance reaches 0 the card status changes to `redeemed`.","tags":["Gift Cards"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemGiftCardRequest"},"example":{"code":"ABCD-1234-EFGH-5678","amount":2500,"transaction_id":"550e8400-e29b-41d4-a716-446655440002","notes":"Partial redemption at checkout"}}}},"responses":{"200":{"description":"Gift card redeemed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"gift_card_id":{"type":"string","format":"uuid"},"amount_redeemed":{"type":"number"},"new_balance":{"type":"number"},"status":{"type":"string","enum":["active","redeemed"]}}}}}}}},"400":{"description":"Invalid code or insufficient balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_code":{"summary":"Invalid code","value":{"error":"Invalid gift card code"}},"insufficient":{"summary":"Insufficient balance","value":{"error":"Insufficient balance. Available: 1000"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/coupons/redeem":{"post":{"summary":"Redeem coupon","description":"Validate and redeem a discount coupon. Validates: active status, date range, min_purchase_amount, and usage limit. Increments `used_count` atomically; sets status to `exhausted` when max_uses is reached.","tags":["Coupons"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemCouponRequest"},"example":{"code":"FESTIVE20","purchase_amount":10000,"customer_id":"550e8400-e29b-41d4-a716-446655440001","transaction_id":"550e8400-e29b-41d4-a716-446655440002"}}}},"responses":{"200":{"description":"Coupon redeemed. Returns computed discount amount.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"coupon_id":{"type":"string","format":"uuid"},"discount_amount":{"type":"number","description":"Computed discount to apply to the transaction."},"used_count":{"type":"integer"},"status":{"type":"string","enum":["active","exhausted"]}}}}}}}},"400":{"description":"Invalid/expired coupon, usage limit reached, or minimum purchase not met.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid":{"summary":"Invalid coupon","value":{"error":"Invalid or expired coupon code"}},"limit":{"summary":"Usage limit","value":{"error":"Coupon usage limit exceeded"}},"min_purchase":{"summary":"Min purchase","value":{"error":"Minimum purchase amount of 5000 required"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/vouchers/validate":{"post":{"summary":"Validate voucher","description":"Check if a voucher code is valid and compute the discount without redeeming it. Safe to call multiple times. Validates code, date range, and usage limit.","tags":["Vouchers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateVoucherRequest"},"example":{"code":"WELCOME500","purchase_amount":15000}}}},"responses":{"200":{"description":"Voucher is valid. Returns discount details.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"voucher_id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"discount_type":{"type":"string","enum":["percentage","fixed"]},"discount_value":{"type":"number"},"discount_amount":{"type":"number","description":"Computed discount for the given purchase_amount."},"min_purchase_amount":{"type":"number","nullable":true},"max_discount_amount":{"type":"number","nullable":true},"usage_limit":{"type":"integer","nullable":true},"used_count":{"type":"integer"},"valid_until":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Invalid/expired voucher or usage limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/vouchers/redeem":{"post":{"summary":"Redeem voucher","description":"Validate and redeem a voucher in a single atomic transaction. Decrements usage count; sets status to `redeemed` for single-use vouchers.","tags":["Vouchers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemVoucherRequest"},"example":{"code":"WELCOME500","purchase_amount":15000,"customer_id":"550e8400-e29b-41d4-a716-446655440001","transaction_id":"550e8400-e29b-41d4-a716-446655440002"}}}},"responses":{"200":{"description":"Voucher redeemed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"voucher_id":{"type":"string","format":"uuid"},"discount_amount":{"type":"number"},"used_count":{"type":"integer"},"status":{"type":"string","enum":["active","redeemed"]}}}}}}}},"400":{"description":"Invalid/expired voucher, usage limit, or min purchase not met.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loyalty/balance":{"get":{"summary":"Get loyalty points balance","description":"Returns the current loyalty points balance for a customer. Returns 404 if the customer does not belong to this tenant.","tags":["Loyalty"],"parameters":[{"name":"customer_id","in":"query","required":true,"description":"Customer UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Loyalty points balance.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid"},"balance":{"type":"integer","description":"Current redeemable points balance."}}}}}}}},"400":{"description":"customer_id is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Customer not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loyalty/otp":{"post":{"summary":"Generate OTP for points redemption","description":"Generate a 6-digit time-limited OTP to authorise a loyalty points redemption. The OTP is delivered to the customer out-of-band (SMS/email by your platform). Validates sufficient balance before generating.","tags":["Loyalty"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateOtpRequest"},"example":{"customer_id":"550e8400-e29b-41d4-a716-446655440001","points":500}}}},"responses":{"200":{"description":"OTP generated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid"},"points":{"type":"integer"},"otp":{"type":"string","description":"6-digit OTP to share with the customer."},"expires_at":{"type":"string","format":"date-time","description":"OTP expiry time."}}}}}}}},"400":{"description":"Insufficient balance or validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"insufficient":{"summary":"Not enough points","value":{"error":"Insufficient points. Available: 200"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Customer not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loyalty/otp/verify":{"post":{"summary":"Verify OTP and redeem points","description":"Verify the OTP entered by the customer and execute the points redemption atomically. OTP is single-use and time-limited. Returns the new balance after redemption.","tags":["Loyalty"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyOtpRequest"},"example":{"customer_id":"550e8400-e29b-41d4-a716-446655440001","otp":"482913"}}}},"responses":{"200":{"description":"OTP verified and points redeemed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid"},"points_redeemed":{"type":"integer"},"new_balance":{"type":"integer"}}}}}}}},"400":{"description":"Invalid or expired OTP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Customer not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customer-wallet":{"get":{"summary":"Get customer wallet summary","description":"Returns the wallet summary for a customer — total balance and individual bucket details. Wallet mutations are not supported via public API; credits happen via the Transaction API `wallet_bucket` field.","tags":["Customer Wallet"],"parameters":[{"name":"customer_id","in":"query","required":true,"description":"Customer UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Wallet summary.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/WalletSummary"}}}}}},"400":{"description":"customer_id is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customer-wallet/transactions":{"get":{"summary":"List wallet transactions","description":"Returns the transaction history for a customer's wallet. Optionally filter by bucket_id.","tags":["Customer Wallet"],"parameters":[{"name":"customer_id","in":"query","required":true,"description":"Customer UUID.","schema":{"type":"string","format":"uuid"}},{"name":"bucket_id","in":"query","description":"Filter to a specific wallet bucket UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Wallet transaction list.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WalletTransaction"}}}}}}},"400":{"description":"customer_id is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}