541 lines
14 KiB
Markdown
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
|