# MASTER BLUEPRINT ENTERPRISE SYSTEM
## Volume 4 — Global ERD and Django Implementation Pack
### Django + Next.js for Multi-Module Enterprise Platform

**Versi:** 1.0  
**Posisi dokumen:** lanjutan dari Volume 1, 2, dan 3  
**Tujuan utama:** menerjemahkan blueprint domain, workflow, dan authorization menjadi struktur data global dan pola implementasi backend Django yang konsisten, scalable, dan maintainable.

---

## Tujuan Volume 4

Setelah Volume 1 menjelaskan fondasi arsitektur, Volume 2 menjelaskan workflow, dan Volume 3 menjelaskan kontrol akses, maka Volume 4 menjawab pertanyaan praktis:

- seperti apa ERD global sistem ini?
- tabel inti apa saja yang harus ada?
- bagaimana relasi lintas modul seharusnya dibangun?
- seperti apa bentuk model Django yang sehat?
- bagaimana service layer diorganisasi?
- bagaimana authorization diintegrasikan ke Django/DRF?
- bagaimana audit dan transaction boundary diperlakukan?
- bagaimana seed data dan naming convention ditetapkan?

Dokumen ini bukan copy-paste codebase lengkap, tetapi **paket desain backend yang cukup matang untuk menjadi acuan implementasi nyata**.

---

## Cara Menggunakan Volume 4

Volume ini sebaiknya dipakai oleh:
- backend engineer
- solution architect
- technical lead
- QA yang menguji rule backend
- governance / product owner teknis yang ingin memastikan model data dan kontrolnya sehat

Dokumen ini juga menjadi jembatan antara:
- blueprint desain
- implementasi Django
- API contract
- test design

# 1. Prinsip Desain Backend Django

## 1.1 Modular Monolith dengan Domain App

Struktur yang direkomendasikan tetap **modular monolith**.  
Setiap domain utama memiliki app sendiri, tetapi tetap berada dalam satu codebase backend.

Contoh struktur:

```text
apps/
  accounts/
  authorization/
  governance/
  master_data/
  purchasing/
  sales/
  inventory/
  expense/
  finance/
shared/
  db/
  exceptions/
  responses/
  utils/
  types/
```

## 1.2 Pisahkan Struktur Data dari Business Logic
Jangan campur semua logic ke:
- model
- serializer
- viewset

Pola yang direkomendasikan:
- `models.py` → struktur data
- `selectors.py` → query/read
- `services.py` → business logic dan transaksi
- `permissions.py` → integrasi authorization
- `serializers.py` → validation transport + mapping
- `views.py` → orchestration tipis
- `tasks.py` → async work

## 1.3 Service Layer adalah Tempat Workflow
Aksi seperti:
- submit PR
- approve PR
- issue PO
- confirm SO
- post goods receipt
- post journal

harus dilakukan dari **service layer**, bukan langsung dari `save()` model atau viewset logic yang tebal.

## 1.4 Selectors untuk Read Complexity
Query list/report/detail yang kompleks lebih baik dipisahkan ke `selectors.py`, agar:
- mudah diuji
- mudah dioptimalkan
- tidak mencemari service layer

## 1.5 DRF Permission Bukan Satu-satunya Tempat Otorisasi
DRF permission class cocok untuk:
- gate access sebelum endpoint berjalan

Tetapi otorisasi final untuk object/action/context tertentu tetap sebaiknya dipanggil juga di service layer saat transisi bisnis penting terjadi.

# 2. Global ERD Overview

## 2.1 Kelompok Entitas Global

Secara global, entitas sistem dibagi menjadi beberapa lapisan:

### Layer A — Identity and Authorization
- User
- Role
- UserRoleAssignment
- AuthorizationObject
- RolePermission
- RoleContextRule

### Layer B — Governance
- ApprovalRule
- DelegationRule
- AuditLog
- SystemPolicy (opsional)

### Layer C — Organization and Master
- Company
- BusinessUnit
- Branch
- Department
- Warehouse
- Vendor
- Customer
- Item
- ItemCategory
- UOM
- Currency
- TaxCode
- PaymentTerm
- CostCenter
- AccountMasterReference

### Layer D — Purchasing
- PurchaseRequest
- PurchaseRequestLine
- PurchaseOrder
- PurchaseOrderLine
- PurchaseApprovalRecord

### Layer E — Sales
- SalesOrder
- SalesOrderLine
- SalesDelivery
- SalesApprovalRecord
- SalesPricingOverrideRequest

### Layer F — Inventory
- GoodsReceipt
- GoodsReceiptLine
- GoodsIssue
- GoodsIssueLine
- StockTransfer
- StockTransferLine
- StockAdjustment
- StockAdjustmentLine
- StockReservation
- StockMovement
- StockBalance

### Layer G — Expense / Costing
- ExpenseRequest
- ExpenseRequestLine
- ExpenseAllocation
- CostAllocationRule

### Layer H — Finance
- JournalEntry
- JournalEntryLine
- AccountsPayable
- AccountsReceivable
- Payment
- Receipt
- FiscalPeriod
- ReconciliationRecord

## 2.2 Prinsip Relasi Global
- transaksi operasional mereferensikan master
- transaksi turunan mereferensikan transaksi sumber
- finance mereferensikan sumber operasional, tidak mengambil alih ownership domain
- inventory memegang ownership movement stok
- governance tidak mengedit transaksi bisnis, tetapi mengatur rule yang mempengaruhi transaksi

# 3. ERD per Domain — Authorization and Governance

## 3.1 Accounts / Identity

### User
Field minimal:
- id
- email
- username
- full_name
- is_active
- is_staff
- created_at
- updated_at

Catatan:
Gunakan custom user model dari awal.

## 3.2 Authorization

### Role
- id
- code
- name
- description
- is_active
- created_at
- updated_at

### AuthorizationObject
- id
- code
- module
- name
- description
- is_active
- created_at
- updated_at

### UserRoleAssignment
- id
- user_id
- role_id
- valid_from
- valid_to
- is_active
- assigned_by
- notes
- created_at
- updated_at

### RolePermission
- id
- role_id
- authorization_object_id
- action
- is_allowed
- created_at
- updated_at

### RoleContextRule
- id
- role_id
- authorization_object_id nullable
- context_field
- operator
- value (JSON)
- is_active
- created_at
- updated_at

## 3.3 Governance

### ApprovalRule
- id
- module
- object_code
- company_id nullable
- business_unit_id nullable
- min_amount nullable
- max_amount nullable
- approval_level
- approver_role_id
- is_active
- created_at
- updated_at

### DelegationRule
- id
- from_user_id
- to_user_id
- applicable_module nullable
- start_date
- end_date
- is_active
- reason
- created_at
- updated_at

### AuditLog
- id
- actor_id nullable
- module
- object_code
- action
- entity_type
- entity_id
- before_data JSON nullable
- after_data JSON nullable
- metadata JSON nullable
- created_at

## 3.4 Relasi Inti
- user 1..n user_role_assignments
- role 1..n user_role_assignments
- role 1..n role_permissions
- role 1..n role_context_rules
- authorization_object 1..n role_permissions
- authorization_object 1..n role_context_rules
- role 1..n approval_rules

# 4. ERD per Domain — Organization and Master Data

## 4.1 Organization Structure

### Company
- id
- code
- name
- is_active
- created_at
- updated_at

### BusinessUnit
- id
- company_id
- code
- name
- is_active
- created_at
- updated_at

### Branch
- id
- company_id
- business_unit_id nullable
- code
- name
- is_active
- created_at
- updated_at

### Department
- id
- company_id
- business_unit_id nullable
- code
- name
- is_active
- created_at
- updated_at

### Warehouse
- id
- company_id
- business_unit_id nullable
- branch_id nullable
- code
- name
- is_active
- created_at
- updated_at

## 4.2 Master Data Core

### Vendor
- id
- company_id
- vendor_code
- name
- vendor_group nullable
- payment_term_id nullable
- currency_id nullable
- tax_code_id nullable
- is_active
- created_at
- updated_at

### Customer
- id
- company_id
- customer_code
- name
- customer_group nullable
- payment_term_id nullable
- currency_id nullable
- tax_code_id nullable
- is_active
- created_at
- updated_at

### ItemCategory
- id
- company_id nullable
- code
- name
- is_active
- created_at
- updated_at

### UOM
- id
- code
- name
- is_active
- created_at
- updated_at

### Currency
- id
- code
- name
- symbol nullable
- is_active
- created_at
- updated_at

### TaxCode
- id
- company_id nullable
- code
- name
- rate nullable
- is_active
- created_at
- updated_at

### PaymentTerm
- id
- company_id nullable
- code
- name
- days nullable
- is_active
- created_at
- updated_at

### CostCenter
- id
- company_id
- business_unit_id nullable
- code
- name
- is_active
- created_at
- updated_at

### Item
- id
- company_id
- item_code
- name
- item_category_id
- base_uom_id
- item_type
- is_inventory_item
- is_active
- created_at
- updated_at

## 4.3 Relasi Penting
- company 1..n vendor/customer/item/warehouse/business_unit
- business_unit 1..n department/warehouse/cost_center
- item_category 1..n item
- uom 1..n item
- payment_term/tax/currency dapat direferensikan vendor/customer/transaksi

# 5. ERD per Domain — Purchasing

## 5.1 PurchaseRequest
Field yang direkomendasikan:
- id
- request_no
- company_id
- business_unit_id
- department_id nullable
- requester_id
- request_date
- status
- total_amount
- notes nullable
- approved_at nullable
- approved_by nullable
- cancelled_at nullable
- cancelled_by nullable
- created_at
- updated_at

## 5.2 PurchaseRequestLine
- id
- purchase_request_id
- line_no
- item_id nullable
- description
- quantity
- uom_id
- estimated_price
- cost_center_id nullable
- expected_date nullable
- notes nullable
- created_at
- updated_at

## 5.3 PurchaseOrder
- id
- po_no
- company_id
- business_unit_id
- vendor_id
- source_purchase_request_id nullable
- order_date
- currency_id
- payment_term_id nullable
- tax_code_id nullable
- status
- total_amount
- issued_at nullable
- issued_by nullable
- approved_at nullable
- approved_by nullable
- created_at
- updated_at

## 5.4 PurchaseOrderLine
- id
- purchase_order_id
- source_purchase_request_line_id nullable
- line_no
- item_id nullable
- description
- quantity
- uom_id
- unit_price
- line_amount
- expected_date nullable
- notes nullable
- created_at
- updated_at

## 5.5 PurchaseApprovalRecord
- id
- module_object
- document_id
- level
- action
- actor_id
- acted_at
- notes nullable

## 5.6 Relasi Penting
- purchase_request 1..n purchase_request_line
- purchase_order 1..n purchase_order_line
- purchase_order optional ref purchase_request
- purchase_order_line optional ref purchase_request_line

## 5.7 Catatan Desain
- simpan snapshot vendor/payment term/tax bila dibutuhkan untuk kestabilan historis
- jangan hanya mengandalkan join ke master jika histori transaksi bisa berubah

# 6. ERD per Domain — Sales

## 6.1 SalesOrder
- id
- so_no
- company_id
- business_unit_id
- branch_id nullable
- customer_id
- order_date
- requested_delivery_date nullable
- currency_id
- payment_term_id nullable
- tax_code_id nullable
- status
- total_amount
- confirmed_at nullable
- confirmed_by nullable
- created_at
- updated_at

## 6.2 SalesOrderLine
- id
- sales_order_id
- line_no
- item_id
- quantity
- uom_id
- unit_price
- discount_amount
- tax_amount
- line_amount
- warehouse_id nullable
- notes nullable
- created_at
- updated_at

## 6.3 SalesPricingOverrideRequest
- id
- sales_order_id
- requested_by
- requested_at
- reason
- requested_value
- status
- approved_by nullable
- approved_at nullable

## 6.4 SalesDelivery
- id
- delivery_no
- sales_order_id
- warehouse_id
- delivery_date
- status
- shipped_at nullable
- shipped_by nullable
- created_at
- updated_at

## 6.5 Relasi Penting
- sales_order 1..n sales_order_line
- sales_order 1..n sales_delivery
- sales_pricing_override_request n..1 sales_order

# 7. ERD per Domain — Inventory

## 7.1 GoodsReceipt
- id
- receipt_no
- company_id
- warehouse_id
- purchase_order_id nullable
- receipt_date
- status
- posted_at nullable
- posted_by nullable
- created_at
- updated_at

## 7.2 GoodsReceiptLine
- id
- goods_receipt_id
- purchase_order_line_id nullable
- item_id
- quantity
- uom_id
- notes nullable

## 7.3 GoodsIssue
- id
- issue_no
- company_id
- warehouse_id
- sales_delivery_id nullable
- issue_date
- status
- posted_at nullable
- posted_by nullable
- created_at
- updated_at

## 7.4 GoodsIssueLine
- id
- goods_issue_id
- sales_order_line_id nullable
- item_id
- quantity
- uom_id
- notes nullable

## 7.5 StockTransfer
- id
- transfer_no
- company_id
- from_warehouse_id
- to_warehouse_id
- transfer_date
- status
- created_at
- updated_at

## 7.6 StockTransferLine
- id
- stock_transfer_id
- item_id
- quantity
- uom_id
- notes nullable

## 7.7 StockAdjustment
- id
- adjustment_no
- company_id
- warehouse_id
- reason_code
- status
- posted_at nullable
- posted_by nullable
- created_at
- updated_at

## 7.8 StockAdjustmentLine
- id
- stock_adjustment_id
- item_id
- quantity_delta
- uom_id
- notes nullable

## 7.9 StockReservation
- id
- company_id
- warehouse_id
- item_id
- source_type
- source_id
- reserved_quantity
- consumed_quantity
- status
- created_at
- updated_at

## 7.10 StockMovement
- id
- company_id
- warehouse_id
- item_id
- movement_type
- source_type
- source_id
- quantity_in
- quantity_out
- movement_date
- reference_no nullable
- created_at

## 7.11 StockBalance
- id
- company_id
- warehouse_id
- item_id
- on_hand_qty
- reserved_qty
- available_qty
- updated_at

## 7.12 Relasi Penting
- goods_receipt 1..n goods_receipt_line
- goods_issue 1..n goods_issue_line
- stock_transfer 1..n stock_transfer_line
- stock_adjustment 1..n stock_adjustment_line
- movement mereferensikan source dokumen
- balance adalah proyeksi/kondensasi dari movement

# 8. ERD per Domain — Expense / Costing

## 8.1 ExpenseRequest
- id
- expense_no
- company_id
- business_unit_id
- requester_id
- expense_date
- status
- total_amount
- notes nullable
- approved_at nullable
- approved_by nullable
- created_at
- updated_at

## 8.2 ExpenseRequestLine
- id
- expense_request_id
- line_no
- expense_category
- amount
- cost_center_id nullable
- notes nullable
- attachment_ref nullable

## 8.3 ExpenseAllocation
- id
- expense_request_id
- cost_center_id
- amount
- allocation_percentage nullable
- notes nullable

## 8.4 CostAllocationRule
- id
- company_id
- code
- name
- rule_type
- is_active
- created_at
- updated_at

## 8.5 Catatan
ExpenseRequest bisa menjadi sumber:
- payable
- journal
- reimbursement
tergantung desain finance yang dipilih.

# 9. ERD per Domain — Finance

## 9.1 FiscalPeriod
- id
- company_id
- year
- period_no
- start_date
- end_date
- status
- created_at
- updated_at

## 9.2 JournalEntry
- id
- journal_no
- company_id
- fiscal_period_id
- journal_date
- source_type nullable
- source_id nullable
- status
- total_debit
- total_credit
- posted_at nullable
- posted_by nullable
- reversed_from_id nullable
- created_at
- updated_at

## 9.3 JournalEntryLine
- id
- journal_entry_id
- line_no
- account_code
- debit_amount
- credit_amount
- cost_center_id nullable
- notes nullable

## 9.4 AccountsPayable
- id
- ap_no
- company_id
- vendor_id
- source_type nullable
- source_id nullable
- document_date
- due_date nullable
- status
- amount
- outstanding_amount
- posted_at nullable
- created_at
- updated_at

## 9.5 AccountsReceivable
- id
- ar_no
- company_id
- customer_id
- source_type nullable
- source_id nullable
- document_date
- due_date nullable
- status
- amount
- outstanding_amount
- posted_at nullable
- created_at
- updated_at

## 9.6 Payment
- id
- payment_no
- company_id
- payable_id nullable
- bank_account_ref nullable
- payment_date
- status
- amount
- posted_at nullable
- created_at
- updated_at

## 9.7 Receipt
- id
- receipt_no
- company_id
- receivable_id nullable
- bank_account_ref nullable
- receipt_date
- status
- amount
- posted_at nullable
- created_at
- updated_at

## 9.8 ReconciliationRecord
- id
- company_id
- reconciliation_type
- reference_no
- status
- reconciled_at nullable
- reconciled_by nullable
- created_at
- updated_at

# 10. Relasi Lintas Modul yang Harus Dijaga

## 10.1 Purchasing → Inventory
- PurchaseOrder menjadi acuan GoodsReceipt
- PurchaseOrderLine menjadi acuan GoodsReceiptLine
- GoodsReceipt memengaruhi StockMovement dan StockBalance

## 10.2 Sales → Inventory
- SalesOrder / SalesDelivery menjadi acuan GoodsIssue
- GoodsIssue memengaruhi StockMovement dan StockBalance
- SalesOrder dapat memicu StockReservation

## 10.3 Purchasing / Expense → Finance
- Purchase/expense flow dapat menghasilkan AccountsPayable
- AP dapat menjadi sumber Payment
- AP dapat memicu JournalEntry sesuai desain

## 10.4 Sales → Finance
- Sales flow dapat menghasilkan AccountsReceivable
- AR dapat menjadi sumber Receipt
- AR dapat memicu JournalEntry sesuai desain

## 10.5 Inventory → Finance
- Pada fase lanjutan, movement tertentu dapat menghasilkan valuation / journal
- Pada fase awal, integrasi ini bisa dibuat lebih sederhana

## 10.6 Governance → Semua Modul
- approval rule dan authorization berlaku lintas modul
- audit log dapat merekam semua transisi penting

# 11. Base Abstract Models yang Direkomendasikan

Agar model konsisten, gunakan abstract base models.

## 11.1 TimeStampedModel
```python
class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
```

## 11.2 ActivatableModel
```python
class ActivatableModel(models.Model):
    is_active = models.BooleanField(default=True)

    class Meta:
        abstract = True
```

## 11.3 CodeNameModel
```python
class CodeNameModel(models.Model):
    code = models.CharField(max_length=100)
    name = models.CharField(max_length=255)

    class Meta:
        abstract = True
```

## 11.4 DocumentStatusMixin
Bisa dipakai untuk entitas dokumen yang punya status.
Tetapi gunakan dengan hati-hati; jangan sampai mixin terlalu pintar dan menyembunyikan business rule.

# 12. Contoh Django Models — Authorization Pack

Contoh berikut bersifat representatif. Dalam implementasi nyata, setiap model diletakkan di app domain yang sesuai.

## 12.1 Custom User
```python
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    full_name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username"]

    def __str__(self):
        return self.full_name or self.email
```

## 12.2 Role
```python
class Role(TimeStampedModel):
    code = models.CharField(max_length=100, unique=True)
    name = models.CharField(max_length=150)
    description = models.TextField(blank=True, null=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "roles"
        ordering = ["name"]
```

## 12.3 AuthorizationObject
```python
class AuthorizationObject(TimeStampedModel):
    MODULE_CHOICES = [
        ("governance", "Governance"),
        ("master_data", "Master Data"),
        ("purchasing", "Purchasing"),
        ("sales", "Sales"),
        ("inventory", "Inventory"),
        ("expense", "Expense"),
        ("finance", "Finance"),
    ]

    code = models.CharField(max_length=100, unique=True)
    module = models.CharField(max_length=50, choices=MODULE_CHOICES)
    name = models.CharField(max_length=150)
    description = models.TextField(blank=True, null=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "authorization_objects"
        ordering = ["module", "code"]
```

## 12.4 UserRoleAssignment
```python
class UserRoleAssignment(TimeStampedModel):
    user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="role_assignments")
    role = models.ForeignKey("Role", on_delete=models.CASCADE, related_name="user_assignments")
    valid_from = models.DateField(blank=True, null=True)
    valid_to = models.DateField(blank=True, null=True)
    is_active = models.BooleanField(default=True)
    assigned_by = models.ForeignKey(
        "User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="assigned_roles",
    )
    notes = models.TextField(blank=True, null=True)

    class Meta:
        db_table = "user_role_assignments"
```

## 12.5 RolePermission
```python
class RolePermission(TimeStampedModel):
    role = models.ForeignKey("Role", on_delete=models.CASCADE, related_name="permissions")
    authorization_object = models.ForeignKey(
        "AuthorizationObject",
        on_delete=models.CASCADE,
        related_name="role_permissions"
    )
    action = models.CharField(max_length=30)
    is_allowed = models.BooleanField(default=True)

    class Meta:
        db_table = "role_permissions"
        unique_together = ("role", "authorization_object", "action")
```

## 12.6 RoleContextRule
```python
class RoleContextRule(TimeStampedModel):
    role = models.ForeignKey("Role", on_delete=models.CASCADE, related_name="context_rules")
    authorization_object = models.ForeignKey(
        "AuthorizationObject",
        on_delete=models.CASCADE,
        related_name="context_rules",
        null=True,
        blank=True,
    )
    context_field = models.CharField(max_length=100)
    operator = models.CharField(max_length=30, default="EQUALS")
    value = models.JSONField()
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "role_context_rules"
```

# 13. Contoh Django Models — Organization and Operational Core

## 13.1 Company and BusinessUnit
```python
class Company(TimeStampedModel):
    code = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=255)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "companies"


class BusinessUnit(TimeStampedModel):
    company = models.ForeignKey("Company", on_delete=models.CASCADE, related_name="business_units")
    code = models.CharField(max_length=50)
    name = models.CharField(max_length=255)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "business_units"
        unique_together = ("company", "code")
```

## 13.2 Vendor and Item
```python
class Vendor(TimeStampedModel):
    company = models.ForeignKey("Company", on_delete=models.PROTECT, related_name="vendors")
    vendor_code = models.CharField(max_length=50)
    name = models.CharField(max_length=255)
    vendor_group = models.CharField(max_length=100, blank=True, null=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "vendors"
        unique_together = ("company", "vendor_code")


class Item(TimeStampedModel):
    company = models.ForeignKey("Company", on_delete=models.PROTECT, related_name="items")
    item_code = models.CharField(max_length=50)
    name = models.CharField(max_length=255)
    item_type = models.CharField(max_length=50, default="GOODS")
    is_inventory_item = models.BooleanField(default=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        db_table = "items"
        unique_together = ("company", "item_code")
```

## 13.3 PurchaseRequest
```python
class PurchaseRequest(TimeStampedModel):
    STATUS_CHOICES = [
        ("DRAFT", "Draft"),
        ("SUBMITTED", "Submitted"),
        ("UNDER_REVIEW", "Under Review"),
        ("APPROVED", "Approved"),
        ("REJECTED", "Rejected"),
        ("CANCELLED", "Cancelled"),
        ("CLOSED", "Closed"),
    ]

    request_no = models.CharField(max_length=50, unique=True)
    company = models.ForeignKey("Company", on_delete=models.PROTECT, related_name="purchase_requests")
    business_unit = models.ForeignKey("BusinessUnit", on_delete=models.PROTECT, related_name="purchase_requests")
    requester = models.ForeignKey("User", on_delete=models.PROTECT, related_name="purchase_requests")
    request_date = models.DateField()
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="DRAFT")
    total_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    notes = models.TextField(blank=True, null=True)

    class Meta:
        db_table = "purchase_requests"
        ordering = ["-created_at"]
```

## 13.4 PurchaseRequestLine
```python
class PurchaseRequestLine(TimeStampedModel):
    purchase_request = models.ForeignKey(
        "PurchaseRequest",
        on_delete=models.CASCADE,
        related_name="lines"
    )
    line_no = models.PositiveIntegerField()
    item = models.ForeignKey("Item", on_delete=models.PROTECT, null=True, blank=True)
    description = models.TextField()
    quantity = models.DecimalField(max_digits=18, decimal_places=2)
    estimated_price = models.DecimalField(max_digits=18, decimal_places=2, default=0)

    class Meta:
        db_table = "purchase_request_lines"
        unique_together = ("purchase_request", "line_no")
```

# 14. Naming Convention dan Design Rules

## 14.1 Penamaan Model
- gunakan nama domain yang jelas
- hindari singkatan yang tidak universal
- gunakan `PurchaseRequest`, bukan `PRHdr` atau `TblPR`

## 14.2 Penamaan Table
- snake_case plural atau konsisten sesuai standar tim
- hindari nama tabel generik seperti `data1`, `master_table`

## 14.3 Penamaan ForeignKey
- gunakan nama domain yang bermakna: `company`, `vendor`, `purchase_order`
- hindari `company_id_ref_new` dan nama serupa

## 14.4 Document Number
Nomor bisnis seperti:
- request_no
- po_no
- so_no
- journal_no

harus dipisahkan dari primary key internal.

## 14.5 Source Reference
Untuk transaksi turunan, pertimbangkan pola:
- `source_type`
- `source_id`

tetapi jika referensinya sudah jelas dan stabil, foreign key eksplisit lebih baik.

## 14.6 Snapshot vs Live Reference
Untuk field seperti:
- vendor name
- payment term text
- tax rate
- currency symbol

pertimbangkan apakah perlu snapshot di dokumen agar histori tetap stabil bila master berubah.

# 15. Service Layer Pattern

Service layer adalah jantung workflow backend.

## 15.1 Kategori Service

### Command Services
Untuk aksi yang mengubah keadaan:
- create_purchase_request
- submit_purchase_request
- approve_purchase_request
- create_purchase_order
- issue_purchase_order
- create_sales_order
- confirm_sales_order
- post_goods_receipt
- post_journal_entry

### Query Selectors
Untuk membaca:
- list_purchase_requests
- get_purchase_request_detail
- list_stock_balance
- get_sales_order_detail

### Domain Helpers
Untuk validasi dan utilitas:
- validate_pr_submit
- compute_pr_total
- resolve_approval_rule
- lock_stock_balance_row_if_needed

## 15.2 Prinsip Service
- satu service untuk satu aksi bisnis yang jelas
- service mengelola transaction boundary
- service memanggil authorization check
- service memvalidasi status awal
- service memvalidasi transisi
- service mencatat audit
- service menjalankan side effect secara terkontrol

## 15.3 Contoh Bentuk Service
```python
@transaction.atomic
def submit_purchase_request(*, actor, purchase_request_id):
    pr = (
        PurchaseRequest.objects
        .select_for_update()
        .select_related("company", "business_unit", "requester")
        .get(id=purchase_request_id)
    )

    authorize_or_raise(
        user=actor,
        object_code="PURCHASE_REQUEST",
        action="SUBMIT",
        context={
            "company_code": pr.company.code,
            "business_unit_code": pr.business_unit.code,
            "owner_user_id": pr.requester_id,
        },
    )

    if pr.status != "DRAFT":
        raise DomainError("Only draft purchase request can be submitted.")

    if not pr.lines.exists():
        raise DomainError("Purchase request must have at least one line.")

    pr.status = "SUBMITTED"
    pr.save(update_fields=["status", "updated_at"])

    write_audit_log(
        actor=actor,
        module="purchasing",
        object_code="PURCHASE_REQUEST",
        action="SUBMIT",
        entity=pr,
    )

    return pr
```

## 15.4 select_for_update pada Aksi Sensitif
Gunakan `select_for_update()` pada:
- approval
- posting
- stock movement
- close/reverse
- payment/receipt posting
agar state tidak rusak karena race condition.

# 16. Authorization Service Pattern

## 16.1 Fungsi yang Direkomendasikan
- `get_active_roles(user)`
- `get_allowed_actions(user, object_code, context=None)`
- `authorize(user, object_code, action, context=None)`
- `authorize_or_raise(...)`
- `evaluate_context_rules(...)`

## 16.2 Contoh `get_active_roles`
```python
from django.utils import timezone

def get_active_roles(user):
    today = timezone.now().date()

    assignments = (
        user.role_assignments
        .filter(is_active=True, role__is_active=True)
        .select_related("role")
    )

    roles = []
    for assignment in assignments:
        if assignment.valid_from and assignment.valid_from > today:
            continue
        if assignment.valid_to and assignment.valid_to < today:
            continue
        roles.append(assignment.role)
    return roles
```

## 16.3 Contoh `authorize`
```python
from django.db.models import Q

def authorize(user, object_code, action, context=None):
    context = context or {}
    roles = get_active_roles(user)

    for role in roles:
        has_perm = role.permissions.filter(
            authorization_object__code=object_code,
            action=action,
            is_allowed=True,
            authorization_object__is_active=True,
        ).exists()

        if not has_perm:
            continue

        rules = role.context_rules.filter(is_active=True).filter(
            Q(authorization_object__code=object_code) |
            Q(authorization_object__isnull=True)
        )

        if not rules.exists():
            return True

        if evaluate_context_rules(rules, context):
            return True

    return False
```

## 16.4 Contoh `authorize_or_raise`
```python
class PermissionDeniedError(Exception):
    pass

def authorize_or_raise(*, user, object_code, action, context=None):
    if not authorize(user, object_code, action, context=context):
        raise PermissionDeniedError(
            f"User is not allowed to perform {action} on {object_code}"
        )
```

## 16.5 Catatan Implementasi
- context harus dikirim eksplisit dari service
- jangan berharap authorization engine menebak semua context sendiri
- untuk list endpoint, gunakan selector filter berdasarkan context bila perlu

# 17. DRF Permission Integration Pattern

## 17.1 Tujuan
- memberikan gate awal di endpoint
- mencegah akses kasar sebelum logic service berjalan
- membantu konsistensi REST layer

## 17.2 Contoh Permission Class Generik
```python
from rest_framework.permissions import BasePermission

class HasObjectActionPermission(BasePermission):
    object_code = None
    action = None

    def has_permission(self, request, view):
        if not self.object_code or not self.action:
            return False

        context = {}
        if hasattr(view, "build_permission_context"):
            context = view.build_permission_context(request)

        return authorize(
            request.user,
            self.object_code,
            self.action,
            context=context,
        )
```

## 17.3 Contoh Penggunaan
```python
class CanCreatePurchaseRequest(HasObjectActionPermission):
    object_code = "PURCHASE_REQUEST"
    action = "CREATE"
```

## 17.4 Catatan Penting
- permission class bagus untuk gate awal
- tetapi service tetap memanggil `authorize_or_raise`
- khusus transisi status, object-level context harus dihitung dari data aktual, bukan hanya request payload

# 18. Selector Pattern

Selector membantu memisahkan query read dari logic command.

## 18.1 Contoh Selector
```python
def list_purchase_requests_for_user(*, user, filters=None):
    filters = filters or {}
    roles = get_active_roles(user)

    qs = PurchaseRequest.objects.select_related("company", "business_unit", "requester")

    # contoh sederhana, implementasi nyata perlu rules yang lebih rapi
    allowed_company_codes = get_allowed_context_values(
        roles=roles,
        object_code="PURCHASE_REQUEST",
        context_field="company_code",
    )

    if allowed_company_codes:
        qs = qs.filter(company__code__in=allowed_company_codes)

    if filters.get("status"):
        qs = qs.filter(status=filters["status"])

    return qs.order_by("-created_at")
```

## 18.2 Kapan Pakai Selector
- list endpoint
- detail endpoint kompleks
- dashboard query
- report query
- search/filter

## 18.3 Kapan Jangan Pakai Selector
- jangan taruh workflow transisi status di selector

# 19. Transaction Boundary Pattern

## 19.1 Kapan Wajib `transaction.atomic`
Gunakan `transaction.atomic` pada:
- create dokumen + line
- submit / approve / reject / cancel / close
- issue / receive / post / reverse
- update stock balance
- create payment / receipt / journal effect

## 19.2 Satu Aksi Bisnis, Satu Boundary yang Jelas
Contoh:
- `approve_purchase_order()` harus atomic
- `post_goods_receipt()` harus atomic
- `post_journal_entry()` harus atomic

## 19.3 Hindari Transaction yang Terlalu Luas
Jangan mengurung proses yang:
- memanggil layanan eksternal lambat
- generate file besar
- kirim email
- hitungan berat non-kritis

Untuk itu, gunakan task async di luar boundary utama jika memungkinkan.

## 19.4 Idempotency
Aksi seperti:
- post
- approve
- issue
- receipt
harus tahan terhadap request ganda.  
Minimal dengan:
- validasi status
- locking
- unique business key

# 20. Audit Logging Strategy

## 20.1 Kapan Audit Ditulis
Tuliskan audit untuk aksi seperti:
- CREATE
- UPDATE penting
- SUBMIT
- APPROVE
- REJECT
- CANCEL
- ISSUE
- RECEIVE
- POST
- REVERSE
- ASSIGN
- DEACTIVATE / ACTIVATE

## 20.2 Bentuk Helper Audit
```python
def write_audit_log(*, actor, module, object_code, action, entity, before_data=None, after_data=None, metadata=None):
    AuditLog.objects.create(
        actor=actor,
        module=module,
        object_code=object_code,
        action=action,
        entity_type=entity.__class__.__name__,
        entity_id=str(entity.pk),
        before_data=before_data,
        after_data=after_data,
        metadata=metadata or {},
    )
```

## 20.3 Apa yang Tidak Perlu Diaudit Terlalu Berat di Awal
- view biasa
- list biasa
- filter biasa
kecuali ada alasan audit khusus

## 20.4 Audit Tidak Sama dengan Django Admin Log
Jangan mengandalkan log admin default sebagai audit bisnis utama.

# 21. Seeder Strategy

Seeder penting untuk mempercepat implementasi dan menjaga konsistensi environment.

## 21.1 Seeder yang Direkomendasikan
- companies
- business_units
- warehouses dasar
- authorization_objects
- role families
- default actions
- default role permissions
- default role context templates
- approval_rules awal
- master minimal (currency, UOM, payment term, tax code)

## 21.2 Urutan Seeder
1. organization master
2. authorization objects
3. roles
4. role permissions
5. role context templates
6. approval rules
7. operational master minimal

## 21.3 Format Seeder
Bisa berupa:
- Django management command
- fixture JSON/YAML
- Python seeding module

Untuk proyek serius, management command Python biasanya paling fleksibel.

# 22. Testing Strategy yang Diturunkan dari Backend Design

## 22.1 Test Level
- model tests terbatas untuk constraint penting
- service tests untuk workflow
- selector tests untuk filter/context
- permission tests untuk role/object/action/context
- integration tests untuk endpoint kritikal

## 22.2 Service Test yang Wajib
Contoh:
- submit PR dari draft sukses
- submit PR non-draft gagal
- approve PR tanpa permission gagal
- create PO dari PR approved sukses
- post goods receipt menambah stock movement
- post journal menolak jika period tertutup

## 22.3 Permission Test yang Wajib
- user tanpa role → deny
- user dengan role tanpa object/action → deny
- user dengan role + action tapi context salah → deny
- user dengan role + action + context benar → allow
- multi-role union permission → allow jika salah satu role match dan context valid

## 22.4 Data Isolation Test
- role company 1000 tidak bisa akses data company 2000
- warehouse staff WH-A tidak bisa post ke WH-B jika context membatasi

# 23. Integration Notes untuk Frontend

Walaupun Volume 4 fokus backend, ada beberapa kontrak penting untuk frontend.

## 23.1 Permission Summary Endpoint
Frontend akan sangat terbantu dengan endpoint seperti:
- `/me/permissions`
- `/me/profile`
- `/me/modules`

## 23.2 Metadata untuk Dropdown dan Form
Backend sebaiknya menyediakan metadata endpoint untuk:
- company options
- business unit options
- warehouse options
- vendor/customer/item lookup
- status options bila perlu

## 23.3 Jangan Biarkan Frontend Menentukan Rule Final
Frontend boleh:
- menyembunyikan tombol
- memvalidasi input dasar
- membatasi pilihan UI

Tetapi backend harus tetap memvalidasi:
- status
- permission
- context
- workflow transition

# 24. Rekomendasi Kode App-by-App

## 24.1 authorization/
Sebaiknya berisi:
- models.py
- services.py
- rules.py
- permissions.py
- selectors.py
- seeds.py

## 24.2 purchasing/
Sebaiknya berisi:
- models.py
- services/
  - purchase_request.py
  - purchase_order.py
- selectors.py
- serializers.py
- permissions.py
- views.py
- tests/

## 24.3 inventory/
Bisa dipisah:
- services/goods_receipt.py
- services/goods_issue.py
- services/stock_adjustment.py
- services/stock_transfer.py

## 24.4 finance/
Pisahkan:
- services/journal.py
- services/payable.py
- services/receivable.py
- services/payment.py
- services/receipt.py
- services/period.py

## 24.5 shared/
Taruh:
- domain exceptions
- helper response
- pagination utils
- common choices/constants

# 25. Anti-Pattern yang Harus Dihindari

## 25.1 Hardcode Role Name di Banyak Tempat
Hindari:
```python
if request.user.role == "manager":
    ...
```

Gunakan authorization service.

## 25.2 Menaruh Workflow di Serializer
Serializer bukan tempat ideal untuk approval/posting workflow.

## 25.3 Menaruh Semua Logic di Model `save()`
Ini membuat side effect sulit dilacak dan diuji.

## 25.4 Menaruh Semua Query di ViewSet
View akan cepat gemuk dan sulit dirawat.

## 25.5 Mengandalkan Frontend untuk Context Filter
Selector backend tetap harus memfilter data sesuai context.

## 25.6 Tidak Menyimpan Field Context di Transaksi
Tanpa company/business_unit/warehouse/cost_center yang cukup, context authorization akan lumpuh.

# 26. Checklist Kesiapan Implementasi Backend

Gunakan checklist ini sebelum masuk sprint backend skala besar.

## 26.1 Data
- Apakah ERD inti disepakati?
- Apakah field context cukup?
- Apakah relasi lintas modul jelas?

## 26.2 Authorization
- Apakah role/object/action/context sudah siap di-seed?
- Apakah `authorize()` service sudah dirancang?
- Apakah SoD awal sudah diidentifikasi?

## 26.3 Workflow
- Apakah status transisi inti sudah final awal?
- Apakah service per aksi sudah didaftar?
- Apakah transaction boundary jelas?

## 26.4 Audit
- Apakah audit helper sudah disepakati?
- Apakah aksi sensitif yang wajib diaudit sudah terdaftar?

## 26.5 Testing
- Apakah test plan service dan permission sudah disusun?
- Apakah data isolation test sudah dipikirkan?

Jika sebagian besar jawaban masih “belum”, implementasi akan rentan kacau.

# 27. Kesimpulan Volume 4

Volume 4 adalah jembatan langsung dari blueprint ke codebase backend.

Dokumen ini menetapkan:
- global ERD sistem
- relasi per domain
- pola model Django
- service layer pattern
- authorization service pattern
- DRF integration pattern
- selector pattern
- audit logging strategy
- transaction boundary
- seeder strategy
- testing implications

Dengan Volume 1 sampai Volume 4, tim kini memiliki:
- fondasi arsitektur
- domain dan workflow
- matriks kontrol akses
- desain backend yang implementable

Ini sudah cukup kuat untuk mulai menyusun:
- migration
- models
- services
- permission integration
- seed data
- test plan backend

---

## Langkah Lanjutan yang Direkomendasikan

Dokumen berikut yang paling natural adalah:

### Volume 5 — API Contract and Next.js Application Structure
Isi utamanya:
- endpoint catalogue per modul
- request/response contract
- error contract
- page map
- module navigation
- route guard
- form/list/detail architecture
- frontend state strategy
- permission-aware UI pattern

Volume 5 akan menjembatani backend blueprint ini dengan implementasi aplikasi web yang dipakai user sehari-hari.

