Skip to main content

Data Master Import Guide

Panduan ini menjelaskan end-to-end flow untuk setup data master MStore dari CSV import hingga siap transaksi. Mengikuti SAP S/4HANA architecture dengan prinsip:
Hide Complexity Behind Simplicity UI/UX onboarding tetap super gampang, complexity disembunyikan di belakang engine.

🏗️ Architecture Overview

Three-Layer Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    LAYER 1: DOMAIN ARCHITECTURE                  │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐           │
│  │   MDG    │ │ Finance  │ │   SCM    │ │ Pricing  │           │
│  │  Engine  │ │  (FI/CO) │ │ (MM/SD)  │ │  Engine  │           │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘           │
├─────────────────────────────────────────────────────────────────┤
│                    LAYER 2: MASTER DATA PACK                     │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐           │
│  │   Org    │ │ Material │ │  Vendor  │ │ Customer │           │
│  │Structure │ │ /Product │ │ Supplier │ │          │           │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘           │
├─────────────────────────────────────────────────────────────────┤
│                 LAYER 3: ONBOARDING SIMPLICITY                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Guided Checklist → Smart Validator → Auto-fill Defaults │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

📋 Import Flow Overview


🗂️ PART 1: Import Order (Dependency-Aware)

PhaseOrderCSV FileTarget Table(s)Dependencies
11.11-setup-branch-warehouse.csvbranches, warehouses, branch_warehousesNone
22.12-template-onboarding-category.csvinventory_categoryNone
22.22-template-onboarding-uom.csvuomNone
22.32-template-onboarding-supplier.csvpartners, supplier_details, partner_bank_accountsNone
22.42-template-onboarding-customer.csvpartners, customer_detailsNone
22.52-template-onboarding-chart-of-accounting.csvchart_of_accountsNone
22.62-template-onboarding-users.csvusers, user_mappingbranches, roles
22.72-template-onboarding-inventory.csvinventory, inventory_pricesinventory_category, partners
22.82-template-onboarding-product.csvproduct, product_pricesinventory_category
22.92-template-onboarding-cogs.csvcogsproduct, inventory
22.102-template-onboarding-addons.csvproduct_addonsproduct, inventory
33.13-template-initialize-stock.csvinventory_batches, warehouse_stocks, inventory_movementsinventory, warehouses
44.14-template-approval-flows.csvapproval_flows, approval_settings, approversusers

📁 PART 2: Database Schema (Actual MySQL)

Penting: Schema di bawah ini berdasarkan database MySQL mstore-monolith yang sebenarnya. Pastikan CSV template dan kode import sesuai dengan struktur ini.

Phase 1: Infrastructure

1.1 branches

CREATE TABLE branches (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  branch_code VARCHAR(50) NOT NULL,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  name VARCHAR(255) NOT NULL,
  address TEXT NOT NULL,
  type ENUM('outlet', 'warehouse', 'office') DEFAULT 'outlet',
  currency_code VARCHAR(10) DEFAULT 'IDR',
  timezone VARCHAR(50) DEFAULT 'Asia/Jakarta',
  phone VARCHAR(50),
  email VARCHAR(255),
  default_warehouse_id BIGINT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,
  deleted_at TIMESTAMP
);

1.2 warehouses

CREATE TABLE warehouses (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  location VARCHAR(255),
  warehouse_code VARCHAR(50) NOT NULL,
  merchant_id BIGINT NOT NULL,
  warehouse_type ENUM('distribution_center', 'store', 'transit') NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,
  deleted_at TIMESTAMP
);

1.3 branch_warehouses (Junction Table)

CREATE TABLE branch_warehouses (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  warehouse_id BIGINT NOT NULL,
  warehouse_code VARCHAR(50),
  role ENUM('default', 'backup') DEFAULT 'default',
  priority INT NOT NULL DEFAULT 0,
  is_active TINYINT NOT NULL DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,
  deleted_at TIMESTAMP
);
Perubahan dari dokumentasi sebelumnya:
  • warehouses adalah tabel terpisah, bukan bagian dari branch_warehouses
  • branch_warehouses adalah junction table yang menghubungkan branches dan warehouses
  • Field warehouse_type dan location ada di tabel warehouses, bukan branch_warehouses

Phase 2: Master Data

2.1 inventory_category

CREATE TABLE inventory_category (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  category_code VARCHAR(50) NOT NULL UNIQUE,
  category_name VARCHAR(255) NOT NULL,
  description TEXT,
  is_active TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,
  deleted_at TIMESTAMP
);

2.2 uom (Unit of Measure)

CREATE TABLE uom (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  uom_code VARCHAR(20) NOT NULL UNIQUE,
  uom_name VARCHAR(100) NOT NULL,
  category VARCHAR(50) NOT NULL,
  description TEXT,
  is_base_unit TINYINT NOT NULL DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,
  deleted_at TIMESTAMP
);
Perubahan: Tabel uom sudah ada di database dan harus diisi saat onboarding. Sebelumnya dokumentasi menyebut ini hanya “lookup reference”.

2.3 partners (Supplier & Customer)

CREATE TABLE partners (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  partner_code VARCHAR(50) NOT NULL,
  name VARCHAR(255) NOT NULL,
  partner_type ENUM('customer', 'supplier', 'both') NOT NULL DEFAULT 'customer',
  partner_category VARCHAR(100),
  `group` VARCHAR(100),
  pic_name VARCHAR(255),
  email VARCHAR(255),
  phone VARCHAR(50),
  address VARCHAR(500),
  city VARCHAR(100),
  province VARCHAR(100),
  country VARCHAR(100),
  tax_id VARCHAR(50),
  payment_term VARCHAR(50),
  credit_limit DECIMAL(18,2),
  price_list VARCHAR(50),
  active TINYINT NOT NULL DEFAULT 1,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.4 supplier_details

CREATE TABLE supplier_details (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  partner_id BIGINT NOT NULL,
  partner_code VARCHAR(50),
  supplier_type VARCHAR(100),
  company_name VARCHAR(255),
  pic_name VARCHAR(255),
  owner_name VARCHAR(255),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

2.5 customer_details

CREATE TABLE customer_details (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  partner_id BIGINT NOT NULL,
  partner_code VARCHAR(50),
  customer_group VARCHAR(100),
  payment_term VARCHAR(50),
  credit_limit DECIMAL(18,2),
  price_list VARCHAR(50),
  is_active TINYINT NOT NULL DEFAULT 1,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

2.6 partner_bank_accounts

CREATE TABLE partner_bank_accounts (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  partner_id BIGINT NOT NULL,
  partner_code VARCHAR(50),
  bank_name VARCHAR(100) NOT NULL,
  account_name VARCHAR(255) NOT NULL,
  account_number VARCHAR(50) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Perubahan: Data supplier dan customer disimpan di 3 tabel:
  1. partners - data umum (nama, alamat, kontak)
  2. supplier_details / customer_details - data spesifik per tipe
  3. partner_bank_accounts - data rekening bank

2.7 chart_of_accounts

CREATE TABLE chart_of_accounts (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  code VARCHAR(20) NOT NULL UNIQUE,
  parent_code VARCHAR(20),
  name_account VARCHAR(255) NOT NULL,
  description VARCHAR(500),
  type ENUM('asset', 'liability', 'equity', 'income', 'expense') NOT NULL DEFAULT 'asset',
  account_category VARCHAR(100) NOT NULL DEFAULT 'asset',
  normal_balance ENUM('debit', 'credit') NOT NULL DEFAULT 'debit',
  level INT,
  currency VARCHAR(10) DEFAULT 'IDR',
  report_position VARCHAR(50) DEFAULT 'balance_sheet',
  is_postable TINYINT DEFAULT 1,
  is_active TINYINT DEFAULT 1,
  branch_id BIGINT,
  branch_code VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.8 users

CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_code VARCHAR(50) NOT NULL UNIQUE,
  user_code_alternate VARCHAR(50),
  username VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  phone VARCHAR(50),
  password VARCHAR(255) NOT NULL,
  is_active TINYINT DEFAULT 1,
  is_locked TINYINT DEFAULT 0,
  last_login DATETIME,
  login_attempts INT DEFAULT 0,
  password_changed_at DATETIME,
  avatar_url VARCHAR(500),
  gender ENUM('male', 'female', 'other'),
  date_of_birth DATE,
  address TEXT,
  is_email_verified TINYINT DEFAULT 0,
  verification_token VARCHAR(255),
  reset_token VARCHAR(255),
  reset_token_expiry DATETIME,
  lang VARCHAR(10) DEFAULT 'id',
  theme VARCHAR(20) DEFAULT 'light',
  tz VARCHAR(50) DEFAULT 'Asia/Jakarta',
  bank_name VARCHAR(100),
  bank_account_name VARCHAR(255),
  bank_account_number VARCHAR(50),
  created_by VARCHAR(100),
  updated_by VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at DATETIME
);

2.9 user_mapping

CREATE TABLE user_mapping (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  user_code VARCHAR(50),
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  role_id BIGINT NOT NULL,
  role_code VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);
Perubahan: User disimpan di 2 tabel:
  1. users - data profil user
  2. user_mapping - mapping user ke merchant, branch, dan role

2.10 inventory

CREATE TABLE inventory (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  sku VARCHAR(100) NOT NULL,
  global_sku VARCHAR(100) NOT NULL,
  branch_sku VARCHAR(100) NOT NULL,
  barcode VARCHAR(100),
  name_json JSON,
  description_json JSON,
  category_id BIGINT,
  category_code VARCHAR(50),
  inventory_type ENUM('physical', 'digital', 'service') DEFAULT 'physical',
  modifier VARCHAR(50) DEFAULT 'none',
  tags TEXT,
  img_url VARCHAR(500),
  reorder_level DECIMAL(18,4) DEFAULT 0,
  unit VARCHAR(20) NOT NULL,
  lead_time_days INT DEFAULT 0,
  min_order_qty DECIMAL(18,4) DEFAULT 1,
  notes TEXT,
  is_active TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.11 inventory_prices

CREATE TABLE inventory_prices (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT,
  merchant_code VARCHAR(50),
  branch_id BIGINT,
  branch_code VARCHAR(50),
  inventory_id BIGINT NOT NULL,
  inventory_code VARCHAR(100),
  supplier_id BIGINT,
  supplier_code VARCHAR(50),
  cost DECIMAL(18,4) NOT NULL,
  price DECIMAL(18,4) NOT NULL,
  currency VARCHAR(10) DEFAULT 'IDR',
  valid_from DATETIME,
  valid_to DATETIME,
  is_active TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.12 product

CREATE TABLE product (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  sku VARCHAR(100) NOT NULL,
  global_sku VARCHAR(100) NOT NULL,
  branch_sku VARCHAR(100) NOT NULL,
  name_json JSON,
  description_json JSON,
  category_json TEXT,
  status ENUM('active', 'inactive', 'draft') DEFAULT 'active',
  currency VARCHAR(10) DEFAULT 'IDR',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.13 product_prices

CREATE TABLE product_prices (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT,
  merchant_code VARCHAR(50),
  branch_id BIGINT,
  branch_code VARCHAR(50),
  product_id BIGINT NOT NULL,
  product_code VARCHAR(100),
  price_code VARCHAR(50) NOT NULL,
  price_name VARCHAR(100) NOT NULL,
  margin_percentage DECIMAL(10,4) NOT NULL DEFAULT 0,
  tax_rate DECIMAL(10,4) NOT NULL DEFAULT 0,
  is_active TINYINT NOT NULL DEFAULT 1,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME,
  deleted_at TIMESTAMP
);

2.14 cogs

CREATE TABLE cogs (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  product_id BIGINT NOT NULL,
  product_code VARCHAR(100),
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  component_type ENUM('material', 'overhead', 'labor', 'postage', 'tax') NOT NULL,
  component_ref_id BIGINT,
  component_ref_code VARCHAR(100),
  qty DECIMAL(18,4) NOT NULL,
  unit VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

2.15 product_addons

CREATE TABLE product_addons (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  product_id BIGINT NOT NULL,
  product_code VARCHAR(100),
  inventory_id BIGINT NOT NULL,
  inventory_code VARCHAR(100),
  display_name VARCHAR(255) NOT NULL,
  qty_delta DECIMAL(18,4) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

Phase 3: Stock Initialize

3.1 inventory_batches

CREATE TABLE inventory_batches (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  inventory_id BIGINT NOT NULL,
  inventory_code VARCHAR(100),
  batch_no VARCHAR(100) NOT NULL,
  po_id VARCHAR(100),
  qty_received DECIMAL(18,4) NOT NULL,
  qty_remaining DECIMAL(18,4) NOT NULL,
  unit VARCHAR(20) NOT NULL,
  received_at DATETIME NOT NULL,
  expired_at DATETIME,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

3.2 warehouse_stocks

CREATE TABLE warehouse_stocks (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  warehouse_id BIGINT NOT NULL,
  warehouse_code VARCHAR(50),
  inventory_id BIGINT NOT NULL,
  inventory_code VARCHAR(100),
  inventory_batch_id BIGINT,
  inventory_batch_code VARCHAR(100),
  qty_on_hand DECIMAL(18,4) NOT NULL,
  qty_reserved DECIMAL(18,4) NOT NULL DEFAULT 0,
  unit VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);

3.3 inventory_movements

CREATE TABLE inventory_movements (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  merchant_id BIGINT NOT NULL,
  merchant_code VARCHAR(50),
  branch_id BIGINT NOT NULL,
  branch_code VARCHAR(50),
  warehouse_id BIGINT NOT NULL,
  warehouse_code VARCHAR(50),
  inventory_id BIGINT NOT NULL,
  inventory_code VARCHAR(100),
  batch_id BIGINT,
  batch_code VARCHAR(100),
  movement_type ENUM('in', 'out', 'transfer', 'adjustment') NOT NULL,
  reference_type VARCHAR(50),
  reference_id BIGINT,
  reference_code VARCHAR(100),
  qty DECIMAL(18,4) NOT NULL,
  unit VARCHAR(20) NOT NULL,
  notes TEXT,
  created_by VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP
);
Perubahan: Stock disimpan di warehouse_stocks, bukan langsung di inventory_batches. Ini memungkinkan tracking stock per warehouse.

🔧 PART 3: Code Changes Required

A. Update CSV Template Columns

1. Branch & Warehouse CSV

Sebelumnya:
NO;Branch Code;Branch Name;Warehouse Code;Warehouse Name;Warehouse Type;Location;Role
Seharusnya (sesuai DB):
NO;Branch Code;Branch Name;Branch Address;Branch Type;Currency;Timezone;Warehouse Code;Warehouse Name;Warehouse Type;Warehouse Location;Role;Priority

2. Supplier CSV

Sebelumnya:
NO;Supplier Code;Supplier Type;Supplier Company Name;Supplier PIC Name;Supplier Email;Supplier Phone;Supplier Address;Bank Name;Bank Account Name;Bank Account Number;Owner Name
Seharusnya (sesuai DB - 3 tabel):
NO;Partner Code;Name;Partner Type;Partner Category;Group;PIC Name;Email;Phone;Address;City;Province;Country;Tax ID;Payment Term;Supplier Type;Company Name;Owner Name;Bank Name;Bank Account Name;Bank Account Number

3. Customer CSV

Sebelumnya:
NO;Customer Code;Customer Name;Email;Phone;Address;City;Province;Country;Tax ID;Customer Group;Payment Term;Credit Limit;Price List;Is Active
Seharusnya (sesuai DB - 2 tabel):
NO;Partner Code;Name;Partner Type;Email;Phone;Address;City;Province;Country;Tax ID;Customer Group;Payment Term;Credit Limit;Price List;Is Active

4. Users CSV

Sebelumnya:
NO;Users Code;Users Code Alt;Username;Email;Role;User Email;Phone;User Address;Bank Name;Bank Account Name;Bank Account Number;Gender
Seharusnya (sesuai DB - 2 tabel):
NO;User Code;User Code Alt;Username;Email;Phone;Password;Gender;Date of Birth;Address;Bank Name;Bank Account Name;Bank Account Number;Merchant Code;Branch Code;Role Code

B. Update ONBOARDING_TABLE_PRESETS

File: features/onboarding/types/onboarding.ts
// BEFORE (incorrect)
branch: {
  colHeaders: ['NO', 'Branch Code', 'Branch Name', 'Warehouse Code', 'Warehouse Name', 'Warehouse Type', 'Location', 'Role'],
  // ...
}

// AFTER (correct - sesuai DB)
branch: {
  colHeaders: [
    'NO', 
    'Branch Code', 
    'Branch Name', 
    'Branch Address',
    'Branch Type',
    'Currency',
    'Timezone',
    'Warehouse Code', 
    'Warehouse Name', 
    'Warehouse Type', 
    'Warehouse Location', 
    'Role',
    'Priority'
  ],
  colWidths: [60, 150, 200, 250, 100, 80, 120, 150, 200, 150, 250, 100, 80],
  columns: [
    { data: 0, type: CellType.NUMERIC, isRequired: true },
    { data: 1, type: CellType.TEXT, isRequired: true, serverField: 'branchCode' },
    { data: 2, type: CellType.TEXT, isRequired: true, serverField: 'branchName' },
    { data: 3, type: CellType.TEXT, isRequired: true, serverField: 'branchAddress' },
    { data: 4, type: CellType.DROPDOWN, source: ['outlet', 'warehouse', 'office'], isRequired: true, serverField: 'branchType' },
    { data: 5, type: CellType.TEXT, isRequired: false, serverField: 'currency' },
    { data: 6, type: CellType.TEXT, isRequired: false, serverField: 'timezone' },
    { data: 7, type: CellType.TEXT, isRequired: true, serverField: 'warehouseCode' },
    { data: 8, type: CellType.TEXT, isRequired: true, serverField: 'warehouseName' },
    { data: 9, type: CellType.DROPDOWN, source: ['distribution_center', 'store', 'transit'], isRequired: true, serverField: 'warehouseType' },
    { data: 10, type: CellType.TEXT, isRequired: false, serverField: 'warehouseLocation' },
    { data: 11, type: CellType.DROPDOWN, source: ['default', 'backup'], isRequired: true, serverField: 'role' },
    { data: 12, type: CellType.NUMERIC, isRequired: false, serverField: 'priority' },
  ],
  serverValidationEndpoint: '/api/onboarding/branch/validate',
}

C. Update Server Validation Endpoint

File: server/api/onboarding/[step]/validate.post.ts
export default defineEventHandler(async (event) => {
  const step = event.context.params?.step || 'unknown'
  const body = await readBody<{ rows?: Array<Record<string, any>> }>(event)
  const rows = body.rows ?? []
  
  const errors: APIErrorItem[] = []
  
  if (step === 'branch') {
    for (const row of rows) {
      const rowNumber = Number(row.row) || 0
      if (rowNumber <= 0) continue
      
      // Validate Branch Code format
      if (!row.branchCode || !/^BRN-[A-Z]{3}-[A-Z0-9]+$/.test(row.branchCode)) {
        errors.push({
          field: 'Branch Code',
          message: 'Branch Code harus format: BRN-[MCH]-[CODE]',
          code: 'BRANCH_CODE_INVALID',
          details: { row: rowNumber }
        })
      }
      
      // Validate Branch Address (required)
      if (!row.branchAddress) {
        errors.push({
          field: 'Branch Address',
          message: 'Branch Address wajib diisi',
          code: 'BRANCH_ADDRESS_REQUIRED',
          details: { row: rowNumber }
        })
      }
      
      // Validate Warehouse Code format
      if (!row.warehouseCode || !/^WH-[A-Z]{3}-[A-Z]{3}-[A-Z]{2}-\d{4}-\d+$/.test(row.warehouseCode)) {
        errors.push({
          field: 'Warehouse Code',
          message: 'Warehouse Code harus format: WH-[MCH]-[COUNTRY]-[TYPE]-[YYMM]-[NNN]',
          code: 'WAREHOUSE_CODE_INVALID',
          details: { row: rowNumber }
        })
      }
    }
  }
  
  if (step === 'supplier') {
    for (const row of rows) {
      const rowNumber = Number(row.row) || 0
      if (rowNumber <= 0) continue
      
      // Validate Partner Code format
      if (!row.partnerCode || !/^SUP-[A-Z]{3}-[A-Z]{3}-[A-Z0-9]+$/.test(row.partnerCode)) {
        errors.push({
          field: 'Partner Code',
          message: 'Supplier Code harus format: SUP-[MCH]-[COUNTRY]-[CODE]',
          code: 'SUPPLIER_CODE_INVALID',
          details: { row: rowNumber }
        })
      }
      
      // Validate Email format
      if (row.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(row.email)) {
        errors.push({
          field: 'Email',
          message: 'Format email tidak valid',
          code: 'EMAIL_INVALID',
          details: { row: rowNumber }
        })
      }
    }
  }
  
  // ... validation rules for other steps
  
  return {
    success: errors.length === 0,
    code: errors.length > 0 ? 'VALIDATION_ERROR' : 'OK',
    message: errors.length > 0 ? `Validation errors for ${step}` : 'OK',
    errors: errors.length > 0 ? errors : undefined
  }
})

D. Update Import Logic (Backend Go)

File: internal/domain/onboarding/service.go (contoh)
// ImportBranchWarehouse handles the import of branch and warehouse data
func (s *OnboardingService) ImportBranchWarehouse(ctx context.Context, rows []BranchWarehouseRow) error {
    tx, err := s.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    
    for _, row := range rows {
        // 1. Insert/Update Branch
        branch := &entity.Branch{
            BranchCode:   row.BranchCode,
            MerchantID:   row.MerchantID,
            Name:         row.BranchName,
            Address:      row.BranchAddress,
            Type:         row.BranchType,
            CurrencyCode: row.Currency,
            Timezone:     row.Timezone,
        }
        branchID, err := s.branchRepo.Upsert(ctx, tx, branch)
        if err != nil {
            return err
        }
        
        // 2. Insert/Update Warehouse
        warehouse := &entity.Warehouse{
            WarehouseCode: row.WarehouseCode,
            MerchantID:    row.MerchantID,
            Name:          row.WarehouseName,
            WarehouseType: row.WarehouseType,
            Location:      row.WarehouseLocation,
        }
        warehouseID, err := s.warehouseRepo.Upsert(ctx, tx, warehouse)
        if err != nil {
            return err
        }
        
        // 3. Insert/Update Branch-Warehouse mapping
        branchWarehouse := &entity.BranchWarehouse{
            MerchantID:    row.MerchantID,
            BranchID:      branchID,
            WarehouseID:   warehouseID,
            Role:          row.Role,
            Priority:      row.Priority,
            IsActive:      true,
        }
        _, err = s.branchWarehouseRepo.Upsert(ctx, tx, branchWarehouse)
        if err != nil {
            return err
        }
    }
    
    return tx.Commit()
}

// ImportSupplier handles the import of supplier data (3 tables)
func (s *OnboardingService) ImportSupplier(ctx context.Context, rows []SupplierRow) error {
    tx, err := s.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    
    for _, row := range rows {
        // 1. Insert/Update Partner
        partner := &entity.Partner{
            MerchantID:   row.MerchantID,
            PartnerCode:  row.PartnerCode,
            Name:         row.Name,
            PartnerType:  "supplier",
            Email:        row.Email,
            Phone:        row.Phone,
            Address:      row.Address,
            City:         row.City,
            Province:     row.Province,
            Country:      row.Country,
            TaxID:        row.TaxID,
            PaymentTerm:  row.PaymentTerm,
        }
        partnerID, err := s.partnerRepo.Upsert(ctx, tx, partner)
        if err != nil {
            return err
        }
        
        // 2. Insert/Update Supplier Details
        supplierDetail := &entity.SupplierDetail{
            PartnerID:    partnerID,
            SupplierType: row.SupplierType,
            CompanyName:  row.CompanyName,
            PICName:      row.PICName,
            OwnerName:    row.OwnerName,
        }
        _, err = s.supplierDetailRepo.Upsert(ctx, tx, supplierDetail)
        if err != nil {
            return err
        }
        
        // 3. Insert/Update Bank Account
        if row.BankName != "" {
            bankAccount := &entity.PartnerBankAccount{
                PartnerID:     partnerID,
                BankName:      row.BankName,
                AccountName:   row.BankAccountName,
                AccountNumber: row.BankAccountNumber,
            }
            _, err = s.bankAccountRepo.Upsert(ctx, tx, bankAccount)
            if err != nil {
                return err
            }
        }
    }
    
    return tx.Commit()
}

🎨 PART 4: Onboarding Simplicity Layer

A. Technical Implementation

1. Frontend Architecture

┌─────────────────────────────────────────────────────────────┐
│                    pages/onboarding.vue                      │
│  ┌─────────────────┐  ┌──────────────────────────────────┐  │
│  │ OnboardingCheck │  │        AppSpreadsheet            │  │
│  │     list        │  │  ┌────────────────────────────┐  │  │
│  │                 │  │  │     Handsontable           │  │  │
│  │  ☑ Branch       │  │  │  (Excel-like editing)      │  │  │
│  │  ☐ Category     │  │  └────────────────────────────┘  │  │
│  │  ☐ UOM          │  │  ┌────────────────────────────┐  │  │
│  │  ☐ Supplier     │  │  │   Validation Error Panel   │  │  │
│  │  ...            │  │  └────────────────────────────┘  │  │
│  └─────────────────┘  └──────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2. IndexedDB Persistence

// utils/indexeddb.ts

// Error structure (with rich metadata)
interface ImportError {
  id?: number
  rowId: number
  message: string
  severity: 'warning' | 'error'
  timestamp: number
  field?: string         // Column name
  columnIndex?: number   // Column index (0-based)
  section?: string       // Step ID / context
}

// Add error with metadata
export const addRowError = async (
  domain: ImportDomain,
  rowId: number,
  message: string,
  severity: 'warning' | 'error' = 'error',
  extras?: Partial<Pick<ImportError, 'field' | 'columnIndex' | 'section'>>
): Promise<void> => {
  const tables = getTables(domain)
  await tables.errors.add({
    rowId,
    message,
    severity,
    timestamp: Date.now(),
    ...(extras ?? {}),
  })
}

3. Hybrid Validation Flow


✅ PART 5: Implementation Checklist

For AI Agents: Step-by-Step TODO

Phase 1: Fix Schema Mismatch

  • 1.1 Update ONBOARDING_TABLE_PRESETS di features/onboarding/types/onboarding.ts sesuai schema DB
  • 1.2 Update CSV template files dengan kolom yang benar
  • 1.3 Update server validation endpoints dengan rules yang sesuai

Phase 2: Implement Multi-Table Import

  • 2.1 Branch import → branches + warehouses + branch_warehouses
  • 2.2 Supplier import → partners + supplier_details + partner_bank_accounts
  • 2.3 Customer import → partners + customer_details
  • 2.4 User import → users + user_mapping
  • 2.5 Stock import → inventory_batches + warehouse_stocks + inventory_movements

Phase 3: Validation Rules

  • 3.1 Branch validation (code format, address required)
  • 3.2 Warehouse validation (code format, type enum)
  • 3.3 Supplier validation (code format, email format)
  • 3.4 Customer validation (code format, credit limit numeric)
  • 3.5 Inventory validation (SKU format, category FK)
  • 3.6 Product validation (SKU format)
  • 3.7 COGS validation (product FK, component type enum)
  • 3.8 Stock validation (warehouse FK, batch FK)

Phase 4: Testing

  • 4.1 Test import dengan data valid
  • 4.2 Test import dengan data invalid (trigger errors)
  • 4.3 Test IndexedDB error persistence
  • 4.4 Test rollback saat error di tengah import

Ready for Transaction Checklist

Setelah semua data master di-import, pastikan:
  • Branches - Minimal 1 cabang aktif
  • Warehouses - Minimal 1 gudang per cabang
  • Branch-Warehouse Mapping - Setiap branch punya default warehouse
  • UOM - Unit of measure tersedia
  • Categories - Kategori inventory tersedia
  • Suppliers - Minimal 1 supplier untuk PO
  • Customers - Minimal 1 customer untuk sales
  • Chart of Accounts - Akun lengkap untuk jurnal
  • Users - User dengan role dan branch mapping
  • Inventory - Data inventory dengan harga
  • Products - Data produk dengan harga
  • COGS - Komponen HPP untuk produk
  • Stock - Stok awal di warehouse
  • Approval Flows - Workflow approval aktif

🚀 PART 6: Advanced SAP-Level Import Packs (Future Scope)

Fact: 12 step checklist di atas sudah cukup untuk operasi mid–high tier. Belum mencakup seluruh spektrum ERP global ala SAP S/4HANA. Bagian ini mendefinisikan paket import lanjutan yang opsional dan belum diimplementasikan di UI, tapi menjadi acuan arsitektur jika MStore berkembang ke level ERP Global 10/10.

6.1 Scope: Apa yang sudah dicakup 12 step, apa yang belum

Sudah dicakup 12 step (Core Master Data):
  • Org & Infrastruktur
    • Branches, Warehouses, Branch–Warehouse Mapping
  • Material / Product / Partner / User
    • Inventory Category, UOM
    • Supplier & Customer (Partners + Details + Bank Accounts)
    • Inventory & Product (+ harga dasar)
    • COGS (komponen biaya)
    • Users & User Mapping
  • Finance dasar
    • Chart of Accounts
  • Operasional awal
    • Initial Stock (batches + stocks + movements)
    • Approval Flows
Belum dicakup (level SAP Global):
  • Master Data Governance (MDG) multi-domain
  • Intercompany automation & consolidation
  • Tax & regulatory compliance multi-negara
  • Advanced SCM (WMS/TMS/GTS)
  • Advanced Pricing Engine (SAP-style condition technique)
  • Auditability 10/10 (SOX-grade)
  • Financial & localization global
Bagian berikut mendeskripsikan kandidat Data Import Pack untuk area di atas.

6.2 Advanced Import Pack A – Pricing Conditions

Tujuan: membawa Advanced Pricing Engine mendekati SAP Condition Technique. Contoh konsep CSV (belum diimplementasi, untuk future design):
# 6A - Pricing Conditions (conceptual)
Condition ID;Price List;Condition Type;Customer Group;Item Group;Min Qty;Max Qty;Currency;Amount;Valid From;Valid To;Priority;Is Active
Catatan:
  • Condition Type dapat merepresentasikan: base price, discount, surcharge, promotion, tax override, dll.
  • Data ini nantinya di-resolve oleh Pricing Engine saat:
    • membuat Sales Order
    • membuat Purchase Order
    • menjalankan promo/contract pricing

6.3 Advanced Import Pack B – Tax & Localization Config

Tujuan: menyiapkan pondasi Tax Engine multi-negara dan lokalisasi dokumen. Contoh konsep CSV:
# 6B - Tax & Localization (conceptual)
Country Code;Tax Code;Tax Type;Rate;Valid From;Valid To;Is Withholding;Ledger Basis;Notes
Contoh penggunaan:
  • Indonesia: PPN 11%, PPh 23, PPh 21
  • EU: VAT standard/reduced, OSS rules
  • Singapore: GST
Konfigurasi ini akan digunakan oleh:
  • modul Finance (posting journal dengan tax split)
  • modul Invoicing (perhitungan pajak di invoice)
  • modul Reporting (tax reporting per negara)

6.4 Advanced Import Pack C – Intercompany Config

Tujuan: menyiapkan data dasar untuk Intercompany Automation:
  • auto SO ↔ PO antar legal entity
  • intercompany pricing & tax rules
  • AR/AP netting & elimination
Contoh konsep CSV:
# 6C - Intercompany Config (conceptual)
From Entity;To Entity;IC Customer Code;IC Vendor Code;Currency;Price Policy;Tax Policy;Is Active;Notes
Dengan import ini, engine intercompany bisa:
  • membuat mirror PO dari SO antar entitas
  • menentukan harga transfer (transfer price) standar
  • mengatur kebijakan tax khusus transaksi intercompany

6.5 Advanced Import Pack D – WMS/TMS Master Data

Tujuan: membuka jalan ke Warehouse Management (WMS) dan Transportation Management (TMS) yang lebih maju. Contoh konsep CSV – WMS Bin / Slot:
# 6D1 - WMS Bin (conceptual)
Warehouse Code;Bin Code;Zone;Rack;Level;Max Capacity;Unit;Is Picking Bin;Is Active
Contoh konsep CSV – Route / Carrier (TMS):
# 6D2 - Transport Routes (conceptual)
Route Code;From Location;To Location;Transport Mode;Carrier;Lead Time Days;Default Cost;Currency;Is Active
Master data ini akan dipakai oleh:
  • modul picking/putaway (penempatan & pengambilan stok)
  • perhitungan ETA & biaya pengiriman

6.6 Advanced Import Pack E – Compliance & Audit Config

Beberapa aspek auditability dibangun sebagai engine, bukan import (immutable log, field history). Namun, ada konfigurasi yang masuk akal sebagai import:
# 6E - Audit & Compliance Rules (conceptual)
Rule Code;Domain;Action;Required Approval Level;Requires Comment;Retention Years;Is Active;Notes
Contoh:
  • Perubahan COGS > 10% butuh approval level manajer
  • Void invoice setelah posting butuh alasan wajib dan jejak audit
  • Retensi log keuangan minimal 10 tahun

6.7 Prinsip Desain – Kapan sesuatu jadi Import Pack?

Gunakan import pack hanya jika:
  • [1] Datanya banyak dan berulang (multi-negara, multi-entity, multi-branch)
  • [2] Perubahan massal lebih efisien lewat CSV dibanding UI
  • [3] Butuh versioning / migrasi antar environment (DEV → UAT → PROD)
Jangan jadikan import pack jika:
  • Datanya sangat sedikit dan jarang berubah (1–5 baris global)
  • Lebih aman di-maintain via UI admin dengan audit trail

Summary:
  • 12 step onboarding saat ini = Core Master Data yang wajib untuk operasi harian.
  • Section ini mendefinisikan Advanced SAP-Level Import Packs yang belum diimplementasikan di UI, namun menjadi blueprint saat MStore naik kelas ke ERP global 10/10.