Files
nextjs-elysia-allaos/docs/checklist-phase6-models.md
phaichayon 043edff93a setup
2026-04-26 00:15:22 +07:00

541 lines
14 KiB
Markdown

# 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