setup
This commit is contained in:
540
docs/checklist-phase6-models.md
Normal file
540
docs/checklist-phase6-models.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user