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

14 KiB

Phase 6: Models (TypeScript) - Checklist

Completed Tasks

Customer Model Refactor

  • Analyze existing customer model structure
  • Update customer model with new schema fields
  • Add Contact model types
  • Add Contact sharing visibility fields

Quotation Model Refactor

  • Analyze existing quotation model structure
  • Update quotation model with multi-currency fields
  • Add QuotationItem model types
  • Add QuotationCustomer model types
  • Add revision tracking fields
  • 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:

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:

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

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:

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:

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

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

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

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

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

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:

  • Customer model updated with new schema fields
  • Contact model added with visibility controls
  • Quotation model updated with multi-currency
  • QuotationItem model added
  • QuotationCustomer model added
  • All TypeScript types exported
  • Status flow updated
  • 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