# Phase 6: Models (TypeScript) - Checklist ## โœ… Completed Tasks ### Customer Model Refactor - [x] Analyze existing customer model structure - [x] Update customer model with new schema fields - [x] Add Contact model types - [x] Add Contact sharing visibility fields ### Quotation Model Refactor - [x] Analyze existing quotation model structure - [x] Update quotation model with multi-currency fields - [x] Add QuotationItem model types - [x] Add QuotationCustomer model types - [x] Add revision tracking fields - [x] Add new status flow enums ## ๐Ÿ“ Created Files 1. **`src/modules/customers/model.refactored.ts`** (149 lines) - Updated CustomerModel with new fields - Added ContactModel with visibility controls - Exported TypeScript types 2. **`src/modules/quotations/model.refactored.ts`** (277 lines) - Updated QuotationModel with multi-currency - Added QuotationItemModel - Added QuotationCustomerModel - Exported TypeScript types --- ## ๐Ÿ”‘ Key Changes in Models ### Customer Model Updates **OLD:** ```typescript Customer: t.Object({ id: t.String(), branch: t.String(), // String branch code name: t.String(), email: t.String({ format: "email" }), phone: t.String(), company: t.String(), address: t.String(), status: t.Union([...]), // "active", "inactive", "pending" createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), }) ``` **NEW:** ```typescript Customer: t.Object({ id: t.String(), branchId: t.String(), // UUID of branch name: t.String(), email: t.String({ format: "email" }), phone: t.String(), company: t.String(), address: t.String(), customerStatus: t.Union([...]), // "active", "inactive", "pending" customerType: t.Optional(t.String()), taxId: t.Optional(t.String()), crmCustomerCode: t.String(), // Auto-generated CRM code erpCustomerCode: t.Nullable(t.String()), // Manual ERP code isActive: t.Boolean(), createdBy: t.String(), updatedBy: t.String(), createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), deletedAt: t.Nullable(t.String({ format: "date-time" })), }) ``` ### New Contact Model ```typescript ContactModel: { Contact: t.Object({ id: t.String(), customerId: t.String(), name: t.String(), position: t.Nullable(t.String()), phone: t.Nullable(t.String()), mobile: t.Nullable(t.String()), email: t.Nullable(t.String()), isPrimary: t.Nullable(t.Boolean()), isPublic: t.Boolean(), // Visibility control notes: t.Nullable(t.String()), branchId: t.String(), createdBy: t.String(), createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), }), CreateContact: t.Object({ name: t.String(), position: t.Optional(t.String()), phone: t.Optional(t.String()), mobile: t.Optional(t.String()), email: t.Optional(t.String()), isPrimary: t.Optional(t.Boolean()), notes: t.Optional(t.String()), }), UpdateContact: t.Object({ name: t.Optional(t.String()), position: t.Optional(t.String()), phone: t.Optional(t.String()), mobile: t.Optional(t.String()), email: t.Optional(t.String()), isPrimary: t.Optional(t.Boolean()), isPublic: t.Optional(t.Boolean()), // Can update visibility notes: t.Optional(t.String()), }), } ``` ### Quotation Model Updates **OLD:** ```typescript Quotation: t.Object({ id: t.String(), quotationNumber: t.String(), branch: t.String(), // String branch code customerId: t.String(), customerName: t.String(), date: t.String({ format: "date-time" }), validUntil: t.String({ format: "date-time" }), subtotal: t.Number(), taxRate: t.Number(), taxAmount: t.Number(), totalAmount: t.Number(), status: t.Union([ t.Literal("draft"), t.Literal("sent"), t.Literal("accepted"), t.Literal("rejected"), t.Literal("expired"), ]), notes: t.Optional(t.String()), createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), }); ``` **NEW:** ```typescript Quotation: t.Object({ id: t.String(), code: t.String(), branchId: t.String(), // UUID of branch customerId: t.String(), quotationDate: t.String({ format: "date-time" }), validUntil: t.String({ format: "date-time" }), // Multi-Currency Fields currencyCode: t.String(), // THB, USD, EUR, JPY, CNY exchangeRate: t.Number(), // Exchange rate at creation baseCurrencyAmount: t.Nullable(t.String()), // THB equivalent // Monetary Values (as strings for precision) subtotal: t.String(), discount: t.String(), taxRate: t.Number(), taxAmount: t.String(), totalAmount: t.String(), // Status Flow status: t.Union([ t.Literal("new_job_draft"), t.Literal("new_job_sent"), t.Literal("follow_up"), t.Literal("closed_lost"), t.Literal("awarded"), t.Literal("cancelled"), ]), // Revision Tracking revisionNo: t.Nullable(t.Number()), parentQuotationId: t.Nullable(t.String()), notes: t.Optional(t.String()), createdBy: t.String(), updatedBy: t.String(), createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), }); ``` ### New Quotation Item Model ```typescript QuotationItemModel: { QuotationItem: t.Object({ id: t.String(), quotationId: t.String(), itemNumber: t.String(), productType: t.String(), description: t.String(), quantity: t.String(), unit: t.String(), unitPrice: t.String(), discount: t.String(), discountType: t.Union([t.Literal("amount"), t.Literal("percentage")]), taxRate: t.Number(), totalPrice: t.String(), notes: t.Nullable(t.String()), createdAt: t.String({ format: "date-time" }), updatedAt: t.String({ format: "date-time" }), }), CreateQuotationItem: t.Object({ itemNumber: t.String(), productType: t.String(), description: t.String(), quantity: t.String(), unit: t.String(), unitPrice: t.String(), discount: t.String(), discountType: t.Union([t.Literal("amount"), t.Literal("percentage")]), taxRate: t.Number(), totalPrice: t.String(), notes: t.Optional(t.String()), }), UpdateQuotationItem: t.Object({ itemNumber: t.Optional(t.String()), productType: t.Optional(t.String()), description: t.Optional(t.String()), quantity: t.Optional(t.String()), unit: t.Optional(t.String()), unitPrice: t.Optional(t.String()), discount: t.Optional(t.String()), discountType: t.Optional(t.Union([t.Literal("amount"), t.Literal("percentage")])), taxRate: t.Optional(t.Number()), totalPrice: t.Optional(t.String()), notes: t.Optional(t.String()), }), } ``` ### New Quotation Customer Model ```typescript QuotationCustomerModel: { QuotationCustomer: t.Object({ id: t.String(), quotationId: t.String(), customerId: t.String(), role: t.String(), isPrimary: t.Nullable(t.Boolean()), createdAt: t.String({ format: "date-time" }), }), CreateQuotationCustomer: t.Object({ customerId: t.String(), role: t.String(), isPrimary: t.Optional(t.Boolean()), }), QuotationCustomerList: t.Object({ success: t.Boolean(), data: t.Array(...), count: t.Number(), message: t.Optional(t.String()), }), } ``` --- ## ๐Ÿ“Š Field Changes Summary ### Customer Field Changes | Old Field | New Field | Type Change | Notes | | --------- | ----------------- | ---------------------- | ------------------------- | | `branch` | `branchId` | String โ†’ String (UUID) | Changed from code to UUID | | `status` | `customerStatus` | No change | Renamed for clarity | | N/A | `customerType` | N/A | New optional field | | N/A | `taxId` | N/A | New optional field | | N/A | `crmCustomerCode` | N/A | Auto-generated CRM code | | N/A | `erpCustomerCode` | N/A | Nullable ERP code | | N/A | `isActive` | N/A | Boolean flag | | N/A | `createdBy` | N/A | User who created | | N/A | `updatedBy` | N/A | User who last updated | | N/A | `deletedAt` | N/A | Soft delete timestamp | ### Quotation Field Changes | Old Field | New Field | Type Change | Notes | | ----------------- | -------------------- | ---------------------- | ------------------------------ | | `quotationNumber` | `code` | No change | Renamed for consistency | | `branch` | `branchId` | String โ†’ String (UUID) | Changed from code to UUID | | `customerName` | N/A | Removed | Get from customer table | | `date` | `quotationDate` | No change | Renamed for clarity | | `subtotal` | `subtotal` | Number โ†’ String | Precision handling | | `taxAmount` | `taxAmount` | Number โ†’ String | Precision handling | | `totalAmount` | `totalAmount` | Number โ†’ String | Precision handling | | N/A | `currencyCode` | N/A | Multi-currency support | | N/A | `exchangeRate` | N/A | Exchange rate at creation | | N/A | `baseCurrencyAmount` | N/A | THB equivalent | | N/A | `discount` | N/A | Discount amount | | N/A | `revisionNo` | N/A | Revision tracking | | N/A | `parentQuotationId` | N/A | Parent quotation for revisions | | N/A | `createdBy` | N/A | User who created | | N/A | `updatedBy` | N/A | User who last updated | ### Status Flow Changes **Old Status:** - `draft` - `sent` - `accepted` - `rejected` - `expired` **New Status:** - `new_job_draft` - Initial draft - `new_job_sent` - Sent to customer (locked) - `follow_up` - Follow-up stage - `closed_lost` - Lost - `awarded` - Won - `cancelled` - Cancelled --- ## ๐Ÿงช TypeScript Types ### Customer Types ```typescript type Customer = { id: string; branchId: string; name: string; email: string; phone: string; company: string; address: string; customerStatus: "active" | "inactive" | "pending"; customerType?: string; taxId?: string; crmCustomerCode: string; erpCustomerCode: string | null; isActive: boolean; createdBy: string; updatedBy: string; createdAt: string; updatedAt: string; deletedAt: string | null; }; type Contact = { id: string; customerId: string; name: string; position: string | null; phone: string | null; mobile: string | null; email: string | null; isPrimary: boolean | null; isPublic: boolean; notes: string | null; branchId: string; createdBy: string; createdAt: string; updatedAt: string; }; ``` ### Quotation Types ```typescript type Quotation = { id: string; code: string; branchId: string; customerId: string; quotationDate: string; validUntil: string; currencyCode: "THB" | "USD" | "EUR" | "JPY" | "CNY"; exchangeRate: number; baseCurrencyAmount: string | null; subtotal: string; discount: string; taxRate: number; taxAmount: string; totalAmount: string; status: | "new_job_draft" | "new_job_sent" | "follow_up" | "closed_lost" | "awarded" | "cancelled"; revisionNo: number | null; parentQuotationId: string | null; notes?: string; createdBy: string; updatedBy: string; createdAt: string; updatedAt: string; }; type QuotationItem = { id: string; quotationId: string; itemNumber: string; productType: string; description: string; quantity: string; unit: string; unitPrice: string; discount: string; discountType: "amount" | "percentage"; taxRate: number; totalPrice: string; notes: string | null; createdAt: string; updatedAt: string; }; ``` --- ## ๐Ÿ“‹ Integration with Controllers ### Using Models in Controllers ```typescript import { CustomerModel, ContactModel } from "./model.refactored"; // In controller .post( "/", async ({ body, currentBranchId, userId }) => { try { const customer = await service.createCustomer( { currentBranchId, userId }, body as CreateCustomer, ); return { success: true, data: customer, message: "Customer created successfully", }; } catch (error) { return { success: false, error: "Failed to create customer", details: error instanceof Error ? error.message : "Unknown error", }; } }, { body: CustomerModel.CreateCustomer, // Elysia validation response: t.Union([...]), }, ) ``` --- ## ๐ŸŽฏ Success Criteria Phase 6 is complete when: - [x] Customer model updated with new schema fields - [x] Contact model added with visibility controls - [x] Quotation model updated with multi-currency - [x] QuotationItem model added - [x] QuotationCustomer model added - [x] All TypeScript types exported - [x] Status flow updated - [x] Monetary fields use strings for precision --- ## ๐Ÿ“ Notes ### Precision Handling - All monetary values are now strings to avoid floating-point precision issues - Use decimal.js or similar library for calculations in service layer ### Multi-Currency - Currency codes are limited to: THB, USD, EUR, JPY, CNY - Exchange rate is captured at creation time (immutable) - Base currency (THB) amount is calculated and stored for reporting ### Contact Visibility - `isPublic` flag controls contact visibility - Default is `false` (private) - Only creator can update `isPublic` field ### Status Flow - New status flow supports sales pipeline stages - Sent quotations are locked (require revision to edit) - Revisions are tracked via `revisionNo` and `parentQuotationId` --- ## ๐Ÿ”„ Next Steps ### Phase 7: Testing - [ ] Write unit tests for models - [ ] Test validation schemas - [ ] Test type safety - [ ] Integration testing with controllers - [ ] Manual API testing ### Migration - [ ] Update existing model files to use refactored versions - [ ] Update controller imports - [ ] Test backward compatibility --- **Phase 6 Status**: โœ… **COMPLETE** **Next Phase**: Phase 7 - Testing