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
MCH-BR-YYMMDD-SEQ-RAND-CHK
Components :
Merchant Code (2-4 chars): Kode merchant
Branch Code (2-4 chars): Kode cabang
Date (6 chars): YYMMDD format
Sequence (3-5 chars): Base36 sequence number
Random (4 chars): Crockford Base32 random
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 :
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.