Skip to main content

Transaction Code Generator

Panduan lengkap Transaction Code Generator untuk membuat kode transaksi yang human-readable, unique, dan verifiable.

🎯 Overview

Transaction Code adalah identifier yang:
  • Human-Readable - Mudah dibaca dan diketik
  • Unique - Unik per merchant-branch-date
  • Verifiable - Memiliki checksum untuk validasi
  • Sortable - Tersortir by date & sequence
  • Compact - Relatif pendek (~30 karakter)
Package: pkg/utils/txcode/txcode.go

📊 Code Structure

Format

MCH-BR-YYMMDD-SEQ-RAND-CHK
Components:
  1. Merchant Code (2-4 chars): Kode merchant
  2. Branch Code (2-4 chars): Kode cabang
  3. Date (6 chars): YYMMDD format
  4. Sequence (3-5 chars): Base36 sequence number
  5. Random (4 chars): Crockford Base32 random
  6. Checksum (1 char): Mod10 checksum

Example Codes

TRX-MC01-BRN-MST-OSK00001-251013-5UJW-3
│   │    │                │      │    └─ Checksum
│   │    │                │      └────── Random (4 chars)
│   │    │                └───────────── Sequence (Base36)
│   │    └────────────────────────────── Date (YYMMDD)
│   └─────────────────────────────────── Branch Code
└───────────────────────────────────────── Merchant Code
Real Examples:
MC01-BRN001-251013-A3F-5UJW-7
MC01-BRN002-251013-A3G-KM2P-4
MC02-BRN001-251014-001-XYZW-9

🚀 Quick Start

1. Generate Transaction Code

package main

import (
    "fmt"
    "time"
    "gitlab.com/mushola-store/mstore_backend/pkg/utils/txcode"
)

func main() {
    // Generate code
    code := txcode.GenerateDisplayCode(
        "MC01",           // merchant code
        "BRN-MST-OSK00001", // branch code
        time.Now(),       // timestamp
        1,                // sequence hint
    )
    
    fmt.Printf("Transaction Code: %s\n", code)
    // Output: MC01-BRN-MST-OSK00001-251013-001-5UJW-7
}

2. With Auto Sequence

// Auto-generate sequence from time
code := txcode.GenerateDisplayCode(
    "MC01",
    "BRN001",
    time.Now(),
    0,  // 0 = auto-generate from time
)

fmt.Printf("Code: %s\n", code)
// Output: MC01-BRN001-251013-A3F-KM2P-4

📝 Usage Examples

Transaction Service

package transaction_service

import (
    "context"
    "time"
    "gitlab.com/mushola-store/mstore_backend/pkg/utils/txcode"
)

type TransactionService struct {
    repo TransactionRepository
}

func (s *TransactionService) CreateTransaction(
    ctx context.Context,
    payload PayloadCreateTransaction,
) (*Transaction, error) {
    // Get branch info
    branch, err := s.branchRepo.GetByCode(ctx, payload.BranchCode)
    if err != nil {
        return nil, err
    }
    
    // Get merchant info
    merchant, err := s.merchantRepo.GetByID(ctx, branch.MerchantID)
    if err != nil {
        return nil, err
    }
    
    // Get daily sequence
    sequence, err := s.getNextSequence(ctx, branch.ID, time.Now())
    if err != nil {
        return nil, err
    }
    
    // Generate transaction code
    txCode := txcode.GenerateDisplayCode(
        merchant.MerchantCode,
        branch.BranchCode,
        time.Now(),
        sequence,
    )
    
    // Create transaction
    transaction := &Transaction{
        TransactionCode: txCode,
        BranchID: branch.ID,
        MerchantID: merchant.ID,
        // ... other fields
    }
    
    return transaction, s.repo.Create(ctx, transaction)
}

func (s *TransactionService) getNextSequence(
    ctx context.Context,
    branchID uint64,
    date time.Time,
) (int64, error) {
    // Get count of transactions today
    count, err := s.repo.CountByBranchAndDate(ctx, branchID, date)
    if err != nil {
        return 0, err
    }
    return count + 1, nil
}

Order Service

func (s *OrderService) CreateOrder(
    ctx context.Context,
    payload PayloadCreateOrder,
) (*Order, error) {
    // Generate order code
    orderCode := txcode.GenerateDisplayCode(
        s.merchantCode,
        s.branchCode,
        time.Now(),
        s.getNextOrderSequence(ctx),
    )
    
    order := &Order{
        OrderCode: orderCode,
        CustomerID: payload.CustomerID,
        TotalAmount: payload.TotalAmount,
        Status: "pending",
    }
    
    return order, s.repo.Create(ctx, order)
}

Invoice Service

func (s *InvoiceService) GenerateInvoice(
    ctx context.Context,
    orderID uint64,
) (*Invoice, error) {
    order, err := s.orderRepo.GetByID(ctx, orderID)
    if err != nil {
        return nil, err
    }
    
    // Generate invoice code
    invoiceCode := txcode.GenerateDisplayCode(
        "INV",  // prefix for invoices
        order.BranchCode,
        time.Now(),
        order.ID,  // use order ID as sequence
    )
    
    invoice := &Invoice{
        InvoiceCode: invoiceCode,
        OrderID: orderID,
        Amount: order.TotalAmount,
        DueDate: time.Now().AddDate(0, 0, 30),
    }
    
    return invoice, s.repo.Create(ctx, invoice)
}

🎨 Code Components

1. Merchant Code

Format: 2-4 uppercase alphanumeric characters Examples:
MC01  - Merchant 01
MC02  - Merchant 02
MUSH  - Mushola Store
CAFE  - Cafe Chain
Usage:
merchantCode := merchant.MerchantCode  // from database
if merchantCode == "" {
    merchantCode = "MC"  // default fallback
}

2. Branch Code

Format: 2-10 uppercase alphanumeric characters (with hyphens) Examples:
BR001           - Branch 001
BRN-MST-OSK     - Branch Mushola Store Oskar
JKT-CENTRAL     - Jakarta Central
Usage:
branchCode := branch.BranchCode  // from database
if branchCode == "" {
    branchCode = "BR"  // default fallback
}

3. Date (YYMMDD)

Format: 6-digit date Examples:
251013  - October 13, 2025
251225  - December 25, 2025
260101  - January 1, 2026
Usage:
date := time.Now().Format("060102")  // YYMMDD

4. Sequence (Base36)

Format: 3-5 character Base36 (0-9, A-Z) Capacity:
  • 3 chars: 46,656 codes (36³)
  • 4 chars: 1,679,616 codes (36⁴)
  • 5 chars: 60,466,176 codes (36⁵)
Examples:
001  - Sequence 1
00Z  - Sequence 35
010  - Sequence 36
ZZZ  - Sequence 46,655
Usage:
// Option 1: Use counter
sequence := getNextSequence(ctx, branchID, date)
code := txcode.GenerateDisplayCode(mc, br, time.Now(), sequence)

// Option 2: Auto-generate from time
code := txcode.GenerateDisplayCode(mc, br, time.Now(), 0)

5. Random (Crockford Base32)

Format: 4-character Crockford Base32 Alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U) Purpose: Prevent guessing & add uniqueness Examples:
5UJW
KM2P
XYZW
A1B2
Capacity: 32⁴ = 1,048,576 combinations

6. Checksum (Mod10)

Format: Single digit (0-9) Purpose: Detect typos & validate code Algorithm: Mod10 over all alphanumeric characters Examples:
MC01-BR001-251013-001-5UJW-7  ← checksum = 7
MC01-BR001-251013-001-5UJW-8  ← invalid (wrong checksum)

🔧 Validation

Validate Checksum

func ValidateTransactionCode(code string) bool {
    // Remove last character (checksum)
    if len(code) < 2 {
        return false
    }
    
    body := code[:len(code)-2]  // remove "-X"
    expectedChecksum := code[len(code)-1]
    
    // Calculate checksum
    actualChecksum := txcode.CalculateChecksum(body)
    
    return actualChecksum == expectedChecksum
}

// Usage
if !ValidateTransactionCode(txCode) {
    return errors.New("invalid transaction code")
}

Parse Components

func ParseTransactionCode(code string) (*TransactionCodeInfo, error) {
    parts := strings.Split(code, "-")
    if len(parts) < 6 {
        return nil, errors.New("invalid code format")
    }
    
    return &TransactionCodeInfo{
        MerchantCode: parts[0],
        BranchCode: parts[1],
        Date: parts[2],
        Sequence: parts[3],
        Random: parts[4],
        Checksum: parts[5],
    }, nil
}

// Usage
info, err := ParseTransactionCode("MC01-BR001-251013-001-5UJW-7")
if err != nil {
    return err
}

fmt.Printf("Merchant: %s\n", info.MerchantCode)
fmt.Printf("Branch: %s\n", info.BranchCode)
fmt.Printf("Date: %s\n", info.Date)

📊 Database Schema

Store Transaction Code

CREATE TABLE transactions (
    id BIGINT UNSIGNED PRIMARY KEY,
    transaction_code VARCHAR(64) UNIQUE NOT NULL,
    merchant_id BIGINT UNSIGNED NOT NULL,
    branch_id BIGINT UNSIGNED NOT NULL,
    transaction_date DATE NOT NULL,
    sequence_number INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_tx_code (transaction_code),
    INDEX idx_branch_date (branch_id, transaction_date),
    UNIQUE KEY uk_branch_date_seq (branch_id, transaction_date, sequence_number)
);

Sequence Management

CREATE TABLE transaction_sequences (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    branch_id BIGINT UNSIGNED NOT NULL,
    sequence_date DATE NOT NULL,
    last_sequence INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_branch_date (branch_id, sequence_date)
);

Get Next Sequence

func (r *TransactionRepository) GetNextSequence(
    ctx context.Context,
    branchID uint64,
    date time.Time,
) (int64, error) {
    var seq TransactionSequence
    
    // Try to get existing sequence
    err := r.db.WithContext(ctx).
        Where("branch_id = ? AND sequence_date = ?", branchID, date).
        First(&seq).Error
    
    if err == gorm.ErrRecordNotFound {
        // Create new sequence
        seq = TransactionSequence{
            BranchID: branchID,
            SequenceDate: date,
            LastSequence: 1,
        }
        if err := r.db.Create(&seq).Error; err != nil {
            return 0, err
        }
        return 1, nil
    }
    
    if err != nil {
        return 0, err
    }
    
    // Increment sequence
    result := r.db.Model(&seq).
        Where("id = ?", seq.ID).
        Update("last_sequence", gorm.Expr("last_sequence + 1"))
    
    if result.Error != nil {
        return 0, result.Error
    }
    
    return seq.LastSequence + 1, nil
}

🎯 Best Practices

1. Use Consistent Codes

// ✅ GOOD - Consistent merchant/branch codes
merchantCode := merchant.MerchantCode  // "MC01"
branchCode := branch.BranchCode        // "BR001"

// ❌ BAD - Inconsistent codes
merchantCode := "MERCHANT-01"  // Too long
branchCode := "b1"             // Too short, lowercase

2. Validate Before Save

// ✅ GOOD
txCode := txcode.GenerateDisplayCode(mc, br, time.Now(), seq)
if !ValidateTransactionCode(txCode) {
    return errors.New("generated invalid code")
}
transaction.TransactionCode = txCode

// ❌ BAD - No validation
transaction.TransactionCode = txcode.GenerateDisplayCode(mc, br, time.Now(), seq)

3. Handle Sequence Atomically

// ✅ GOOD - Atomic sequence increment
tx := db.Begin()
sequence, err := getNextSequence(tx, branchID, date)
if err != nil {
    tx.Rollback()
    return err
}
txCode := generateCode(mc, br, time.Now(), sequence)
// ... create transaction
tx.Commit()

// ❌ BAD - Race condition
sequence := getNextSequence(db, branchID, date)  // Not atomic!

4. Index Transaction Codes

-- ✅ GOOD
CREATE INDEX idx_tx_code ON transactions(transaction_code);

-- ❌ BAD - No index, slow lookups
SELECT * FROM transactions WHERE transaction_code = 'MC01-BR001-251013-001-5UJW-7';

🔍 Troubleshooting

Duplicate Codes

Problem: Same code generated twice Cause: Sequence not atomic or clock skew Solution:
// Use database transaction for atomicity
tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

sequence, err := getNextSequence(tx, branchID, date)
if err != nil {
    tx.Rollback()
    return err
}

txCode := generateCode(mc, br, time.Now(), sequence)
transaction.TransactionCode = txCode

if err := tx.Create(&transaction).Error; err != nil {
    tx.Rollback()
    return err
}

tx.Commit()

Invalid Checksum

Problem: Code fails validation Cause: Manual typo or data corruption Detection:
if !ValidateTransactionCode(code) {
    log.Error("Invalid transaction code",
        "code", code,
        "expected_checksum", calculateChecksum(code[:len(code)-2]),
        "actual_checksum", code[len(code)-1],
    )
    return ErrInvalidCode
}

Sequence Reset

Problem: Sequence resets unexpectedly Cause: Date change or database issue Prevention:
-- Ensure unique constraint
ALTER TABLE transaction_sequences
ADD UNIQUE KEY uk_branch_date (branch_id, sequence_date);

-- Monitor sequence gaps
SELECT 
    branch_id,
    sequence_date,
    last_sequence,
    COUNT(*) as transaction_count
FROM transactions
GROUP BY branch_id, sequence_date
HAVING last_sequence != transaction_count;

Snowflake ID Generator

Distributed unique ID generation

Transaction Flow

Complete transaction flow

Database Schema

Database schema & indexes

Transaction Codes: Digunakan untuk display & customer-facing references. Internal ID menggunakan Snowflake IDs untuk uniqueness.