Sentra Membership — Database Design¶
Database dirancang menggunakan PostgreSQL dengan pemisahan tabel transaksi wallet yang diamankan secara kriptografis (signed transactions) untuk mencegah pemalsuan saldo.
4.1 Entity-Relationship Diagram (ERD)¶
erDiagram
MEMBERS {
uuid id PK
varchar member_number UK
varchar full_name
varchar email UK
text biometric_hash
varchar status
}
SUBSCRIPTIONS {
uuid id PK
uuid member_id FK
uuid package_id FK
date start_date
date end_date
varchar status
boolean auto_renew
}
PACKAGES {
uuid id PK
varchar name
numeric price
integer validity_days
integer total_sessions
}
CLASSES {
uuid id PK
uuid trainer_id FK
uuid studio_id FK
timestamp start_time
timestamp end_time
integer max_capacity
integer remaining_slots
}
BOOKINGS {
uuid id PK
uuid class_id FK
uuid member_id FK
timestamp booking_time
varchar status
integer queue_position
}
WALLETS {
uuid id PK
uuid member_id FK
numeric balance
varchar wallet_signature_hash
}
WALLET_TRANSACTIONS {
uuid id PK
uuid wallet_id FK
varchar transaction_type
numeric amount
varchar transaction_signature
timestamp created_at
}
GATE_LOGS {
uuid id PK
uuid member_id FK
uuid branch_id FK
timestamp access_time
varchar access_type
varchar verification_method
}
MEMBERS ||--o{ SUBSCRIPTIONS : has
PACKAGES ||--o{ SUBSCRIPTIONS : defines
MEMBERS ||--|| WALLETS : owns
WALLETS ||--o{ WALLET_TRANSACTIONS : logs
MEMBERS ||--o{ BOOKINGS : books
CLASSES ||--o{ BOOKINGS : has
MEMBERS ||--o{ GATE_LOGS : scans
4.2 Production PostgreSQL DDL Schema¶
Skema DDL SQL lengkap dengan constraint ketat, auto-indexing, dan trigger keamanan saldo wallet.
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 1. Table Members
CREATE TABLE members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
member_number VARCHAR(30) UNIQUE NOT NULL,
full_name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
phone_number VARCHAR(20) NOT NULL,
biometric_hash TEXT, -- One-way face feature vector hash
status VARCHAR(20) CHECK (status IN ('ACTIVE', 'FREEZE', 'INACTIVE')) DEFAULT 'ACTIVE' NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_members_number ON members(member_number);
-- 2. Table Packages & Subscriptions
CREATE TABLE packages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(50) NOT NULL,
price NUMERIC(12,2) NOT NULL CHECK (price >= 0.00),
validity_days INTEGER NOT NULL CHECK (validity_days > 0),
total_sessions INTEGER DEFAULT 0 NOT NULL, -- 0 means unlimited
is_active BOOLEAN DEFAULT TRUE NOT NULL
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
member_id UUID NOT NULL REFERENCES members(id) ON DELETE RESTRICT,
package_id UUID NOT NULL REFERENCES packages(id) ON DELETE RESTRICT,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status VARCHAR(20) CHECK (status IN ('PENDING', 'ACTIVE', 'FROZEN', 'EXPIRED', 'CANCELLED')) DEFAULT 'PENDING' NOT NULL,
auto_renew BOOLEAN DEFAULT FALSE NOT NULL,
subscription_gateway_id VARCHAR(100) UNIQUE, -- Midtrans/Xendit recurring ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_dates CHECK (end_date >= start_date)
);
CREATE INDEX idx_subs_member ON subscriptions(member_id);
-- 3. Table Trainers & Classes
CREATE TABLE trainers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
full_name VARCHAR(100) NOT NULL,
specialization VARCHAR(50) NOT NULL,
base_session_rate NUMERIC(12,2) NOT NULL CHECK (base_session_rate >= 0.00),
is_active BOOLEAN DEFAULT TRUE NOT NULL
);
CREATE TABLE classes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
trainer_id UUID NOT NULL REFERENCES trainers(id) ON DELETE RESTRICT,
name VARCHAR(100) NOT NULL,
branch_id UUID NOT NULL,
studio_name VARCHAR(50) NOT NULL,
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
max_capacity INTEGER NOT NULL CHECK (max_capacity > 0),
remaining_slots INTEGER NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_class_times CHECK (end_time > start_time)
);
CREATE INDEX idx_classes_start ON classes(start_time);
-- 4. Table Bookings (High Concurrent Transactions)
CREATE TABLE bookings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE RESTRICT,
member_id UUID NOT NULL REFERENCES members(id) ON DELETE RESTRICT,
booking_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
status VARCHAR(20) CHECK (status IN ('CONFIRMED', 'WAITING', 'ATTENDED', 'CANCELLED', 'NOSHOW')) DEFAULT 'CONFIRMED' NOT NULL,
queue_position INTEGER DEFAULT 0 NOT NULL,
cancelled_at TIMESTAMP WITH TIME ZONE,
UNIQUE (class_id, member_id)
);
CREATE INDEX idx_bookings_class ON bookings(class_id);
-- 5. Table Wallets & Signed Transactions (Anti-Tampering)
CREATE TABLE wallets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
member_id UUID UNIQUE NOT NULL REFERENCES members(id) ON DELETE RESTRICT,
balance NUMERIC(12,2) DEFAULT 0.00 NOT NULL CHECK (balance >= 0.00),
wallet_signature_hash TEXT NOT NULL, -- SHA256(member_id + balance + secret_salt)
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE wallet_transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wallet_id UUID NOT NULL REFERENCES wallets(id) ON DELETE RESTRICT,
transaction_type VARCHAR(20) CHECK (transaction_type IN ('TOPUP', 'DEBIT_BOOKING', 'REFUND', 'BONUS')) NOT NULL,
amount NUMERIC(12,2) NOT NULL CHECK (amount > 0.00),
transaction_signature VARCHAR(64) NOT NULL, -- Unique hash of transaction details
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
-- 6. Table Gate Access Logs (Anti-Passback Verification)
CREATE TABLE gate_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
member_id UUID NOT NULL REFERENCES members(id) ON DELETE RESTRICT,
branch_id UUID NOT NULL,
access_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
access_type VARCHAR(10) CHECK (access_type IN ('IN', 'OUT')) NOT NULL,
verification_method VARCHAR(20) CHECK (verification_method IN ('QR', 'RFID', 'FACE')) NOT NULL,
is_success BOOLEAN DEFAULT TRUE NOT NULL,
failure_reason VARCHAR(100)
);
CREATE INDEX idx_gatelogs_member ON gate_logs(member_id, access_time DESC);