LabaBersih v2 — Complete Documentation

LabaBersih = Accounting for Online Commerce (A2X versi Indonesia) Seller Indonesia gak perlu pakai 3 app terpisah (marketplace + accounting + inventory). LabaBersih = satu dashboard, semua terurus.
KapabilitasA2XLabaBersih
Accounting (COA, jurnal, laporan)Bridge ke XeroBuilt-in
Per-fee Reconciliation + SuspenseYesYes
PlatformShopify, AmazonTikTok, Shopee, Mengantar
Inventory + FEFONoYes
Order Management + PackingNo (pakai Shopify)Built-in
RTS/Returns + InboundNoYes
PO/PurchasingNoYes
Target MarketGlobal (English)Indonesia (Bahasa, Rupiah, WIB)

Problem yang Diselesaikan

  1. Laba kelihatan besar, ternyata sisa dikit — fee transparency + per-fee reconciliation
  2. Manual import/export antar platform — staging pipeline + API sync
  3. Gak tau HPP akurat — FEFO lot tracking, bukan tebak-tebakan
  4. Laporan keuangan gak beres — Trial Balance enforced, COA immutable, jurnal balanced

Tech Stack

LayerTech
BahasaElixir
FrameworkPhoenix 1.8
UILiveView + Tailwind v4
DatabasePostgreSQL (Fly.io)
AuthGuardian (JWT) + bcrypt
JobsOban
HostingFly.io (~$10-15/bulan)

Status & Phase Map

Yang Sudah Jadi

Phase Map

Domain Phase 1 Sekarang Phase 2 Nanti Phase 3 Scale
Master Data Bundle + unit conv, courier mapping, reserved_stock Multi-warehouse enforcement
Accounting Daily summary journal (Oban) Recurring expenses, aged receivables, financial health
Order Intake XLSX auto-import, SKU importer API staging pipeline (TikTok/Shopee/Mengantar) Settlement XLSX
Fulfillment Keep current + reserved_stock + courier_mapping FO → PickList → Ship → Handover, GRN PackingIncident QC
Returns Done Customer refund/replacement
Intelligence Demand, ABC-XYZ, spike, reorder

Entity Relationships

Organization ├── Legal Entity (1:N) ← tax entity (PT/CV/personal) │ ├── Store (1:N) ← toko marketplace │ │ ├── Account FK ×8 ← piutang, kas, pendapatan, hpp, persediaan, retur, sample, transfer │ │ └── default_warehouse_id → Warehouse │ ├── Purchase Order (1:N) │ ├── Inventory Lot (1:N) ← lot stok milik entity ini │ └── Tax Record (1:N) ← PPh Final bulanan │ ├── Warehouse (1:N) │ └── Warehouse Location (1:N, self-referencing hierarchy) │ └── Zone → Rack → Bin (via parent_id) │ ├── Product (1:N) │ ├── type: simple / bundle │ ├── Bundle Item (1:N) ← BOM: komponen per bundle │ ├── Unit Conversion (1:N) ← kardus=18pcs, lusin=12pcs │ ├── Inventory Lot (1:N) ← per gudang, FEFO │ └── reserved_stock ← committed qty │ ├── Order (1:N) │ ├── Order Item (1:N) ← product + qty + price │ ├── store_id → Store │ └── Customer (denormalized: name, phone di order) │ ├── Courier Mapping (1:N) ← "Shopee Cargo JNE" → "JNE" │ ├── Journal Entry (1:N) │ └── Journal Entry Line (1:N, order_id nullable) │ └── Account / COA (1:N) └── 3 level: Type Header → Platform Header → Store Leaf

Key Relationships

Store N──1 Legal Entity
Store 1──8 Account (FK, auto-generated)
Store N──1 Warehouse (default)
Order N──1 Store
Order Item N──1 Product
Journal Entry Line.order_id → Order (per-order tracking)
Journal Entry.batch_date + batch_type (daily summary)
Legal Entity 1──N Tax Record
Legal Entity 1──N Purchase Order
Legal Entity 1──N Inventory Lot

Domain 1: Master Data

Definisi: Data referensi yang jarang berubah. Fondasi semua operasi lain.

Product — Simple vs Bundle

Designed Phase 1

Prinsip

Schema

-- ALTER products
ADD COLUMN type           VARCHAR NOT NULL DEFAULT 'simple'  -- simple / bundle
ADD COLUMN harga_jual     DECIMAL                            -- untuk margin report
ADD COLUMN reserved_stock INTEGER NOT NULL DEFAULT 0         -- committed qty

-- NEW table: bundle_items (BOM)
bundle_items:
  id                    BINARY_ID PK
  bundle_product_id     FK → products (type=bundle)
  component_product_id  FK → products (type=simple)
  quantity              INTEGER NOT NULL  -- berapa komponen per 1 bundle
  UNIQUE(bundle_product_id, component_product_id)

Bundle Logic

Stok bundle = floor(MIN(komponen.stok / bundle_item.quantity))
HPP bundle  = SUM(komponen.hpp × bundle_item.quantity)

Saat bundle dijual:
  Order: BNDL02 × 2
  Lookup: BNDL02 = FRB01 × 3
  Consume: FRB01 × 6 (FEFO)
  HPP = sum lot costs dari 6 unit FRB01

Stock: On Hand / Committed / Available

On Hand   = product.stok (physical stock in warehouse)
Committed = product.reserved_stock (claimed by orders, not yet shipped)
Available = stok - reserved_stock (can be sold)

reserve_stock(product_id, qty)    → saat order masuk
release_reservation(product_id, qty)  → saat ship atau cancel

Unit Conversion

Designed Phase 1

Prinsip: Database SELALU simpan base unit (pcs). Konversi di application layer.

Schema

unit_conversions:
  id          BINARY_ID PK
  product_id  FK → products
  unit_name   VARCHAR NOT NULL  -- "kardus", "lusin", "pak"
  factor      DECIMAL NOT NULL  -- 1 unit = factor × base unit
  UNIQUE(product_id, unit_name)

Contoh

OperasiUser InputSystem Convert & Simpan
PO10 kardus @ Rp 810.000/kardus180 pcs, lot cost Rp 45.000/pcs
Receive10 kardus ✓180 pcs masuk stok
Order3 pcsconsume 3 pcs dari lot
Display"180 pcs (10 kardus)"

Stock Audit Report

SKU    Produk           Stok(pcs)  Stok(kardus)  Sisa
FRB01  Forbest New 1kg  180        10 kardus     0 pcs    ✓ PAS
FRB02  Forbest 1kg       55         3 kardus     1 pcs    ← sisa
RTN01  Ritrina 1L        42         2 kardus     2 botol  ← sisa

Warehouse & Location Hierarchy

Partial Phase 1

Gudang Kediri (warehouse)
├── Zona Frozen (location, parent=null)
│   ├── Rak A (parent=Zona Frozen, entity=PT Bestari)
│   └── Rak B (parent=Zona Frozen, entity=CV Maju)
└── Zona Packing (location, parent=null)

Rules

Courier Mapping

Designed Phase 1

Problem: Marketplace kirim nama ekspedisi berbeda-beda. "Shopee Cargo JNE", "JNE REG", "JNE ECO" = semua "JNE".
courier_mappings:
  org_id    FK → organizations
  raw_name  VARCHAR  -- "Shopee Cargo JNE" (dari marketplace)
  courier   VARCHAR  -- "JNE" (induk kurir)
  UNIQUE(org_id, raw_name)
Raw Name (dari marketplace)Courier (induk)
Shopee HematJ&T
Shopee Cargo JNEJNE
JNE REG / JNE ECO / JNE CargoJNE
SiCepat REG / SiCepat HaluSiCepat
SPX ExpressSPX

Customer

Decided

Keputusan: Denormalized di order. TIDAK ada tabel terpisah. Seller marketplace gak kenal customernya. Data dari platform. Repeat buyer tracking bisa dari aggregasi customer_phone nanti.
orders.customer_name   -- "Budi"
orders.customer_phone  -- "081234" (nullable)

Store — Relasi Lengkap

Implemented

Store:
├── org_id → Organization
├── legal_entity_id → Legal Entity
├── default_warehouse_id → Warehouse (Phase 1 tambah)
├── platform: "tiktok" / "shopee" / "mengantar" / "pos"
├── fee_mapping: [{label, accountCode, percent, flatAmount}]
└── 8 Account FKs (auto-generated):
    ├── piutang_account_id    → "12-201 Piutang Shopee Bestari"
    ├── revenue_account_id    → "40-101 Pendapatan Shopee Bestari"
    ├── kas_account_id        → "11-311 Saldo Shopee Bestari"
    ├── hpp_account_id        → "50-100"
    ├── persediaan_account_id → "13-100"
    ├── retur_account_id      → "41-101 Retur Shopee Bestari"
    ├── sample_account_id     → nullable
    └── transfer_kas_account_id → nullable (Mengantar only)

Legal Entity

Implemented

Legal Entity:
├── name: "PT Bestari Jaya"
├── entity_type: "pt" / "cv" / "personal"
├── tax_status: "non_pkp" / "pkp"
├── npwp: "01.234.567.8-901.000"
├── is_default: true (1 per org, auto-create saat register)
└── Owns: stores, POs, lots, stock_transactions, tax_records
Pendekatan pragmatis: Default: 1 legal entity per org (auto-create). User baru gak perlu mikir entity. User scale: aktifkan multi-entity, pisahkan toko + persediaan per entity. Data structure SUDAH SIAP — gak perlu restructure.

Domain 2: Accounting

Definisi: Semua yang berhubungan dengan uang, jurnal, laporan, pajak.

COA — 3 Level Hierarchy

Implemented

Level 1: Type header      → 12 Piutang          (is_header=true, source_type="template")
Level 2: Platform header  → 12-2 Piutang Shopee  (source_type="platform")
Level 3: Store leaf       → 12-201 Piutang Shopee Bestari  (source_type="store")

Auto-Generate Flow

1. Org baru → seed COA template (level 1 headers)
2. Toko baru →
   a. Cek platform header ada? Kalau belum → create (level 2)
   b. Create leaf accounts per toko (level 3)
   c. Link store record ke accounts via FK
3. Rekonsiliasi → validasi order milik toko yang dipilih
4. Laporan → aggregate leaf → platform → type

Fundamental Principles

Daily Summary Journal (Opsi 4)

Designed Phase 1

Keputusan: 1 jurnal per toko per hari, lines per order (traceable per rupiah). Scale: 300 order/hari = 6 jurnal (vs 600 per-order approach).

Flow

SAAT SHIP (real-time per order):
  → Stok potong (FEFO lot consumption) ✓
  → HPP tercatat per order ✓
  → Jurnal fee BELUM (batch nanti)

DAILY BATCH (Oban job akhir hari, per toko):
  → 1 jurnal: "Penjualan Shopee Bestari — 30 Mar 2026"
  → Lines per order:
      Dr. Piutang (PS2603-00001)  Rp 184.000
      Dr. Admin   (PS2603-00001)  Rp   6.000  ← fee estimasi
      Dr. Komisi  (PS2603-00001)  Rp  10.000
      Cr. Revenue (PS2603-00001)  Rp 200.000
  → 1 jurnal HPP summary per toko per hari

SETTLEMENT (7-14 hari kemudian):
  → Per-fee matching (estimasi vs aktual)
  → Known fee → auto-adjust ke akun yang benar
  → Unknown fee → suspense account (61-999)
Kenapa bukan pure summary tanpa fee? Revenue 1M tanpa beban tercatat = overconfident. Seller ambil keputusan salah karena laba kelihatan 100%. Ini PROBLEM #1 yang LabaBersih harus SOLVE.

Per-Fee Reconciliation + Suspense

Implemented

Alur:
1. Validate order.store_id == attrs.store_id (ENFORCED)
2. Calculate estimated piutang dari fee mapping × gross
3. Per fee: match estimated vs actual
4. Known fee dengan selisih → adjustment ke akun fee
5. Unknown fee → suspense account 61-999

Cocok (selisih=0):   Dr. Kas / Cr. Piutang
Fee underestimated:  Dr. Kas + Dr. Adjustment / Cr. Piutang
Fee overestimated:   Dr. Kas / Cr. Piutang + Cr. Adjustment
Unknown fee:         → Suspense 61-999 (user kategorikan manual)

Tax — PPh Final

Implemented

Per legal entity per bulan:
  gross_revenue = SUM revenue jurnal entity bulan itu
  tax_payable   = gross_revenue × 0.5%

NON-PKP flow:
  1. Akhir bulan → auto-generate tax_record
  2. Status: draft → filed → paid
  3. PKP threshold warning: omzet YTD / Rp 4.8M

PKP flow: Structure ready (nullable ppn_output/ppn_input), implement nanti.

14 Journal Formulas

A1. Penjualan (saat kirim)

Dr. Piutang {platform}          = NET (gross - fees)
Dr. Biaya Admin Platform         = fee (dari fee mapping)
Dr. Biaya Komisi                 = fee
    Cr. Pendapatan {platform}    = GROSS (totalHarga)

Dr. HPP                          = FEFO lot cost × quantity
    Cr. Persediaan               = FEFO lot cost × quantity

A2. Rekonsiliasi

Cocok:  Dr. Kas / Cr. Piutang
Under:  Dr. Kas + Dr. Adjustment / Cr. Piutang
Over:   Dr. Kas / Cr. Piutang + Cr. Adjustment

A3. RTS — Reverse Penjualan

Dr. Retur Penjualan              = GROSS
    Cr. Piutang {platform}       = NET
    Cr. Biaya Admin              = fee (reverse)
Dr. Persediaan                   = hppTotal
    Cr. HPP                      = hppTotal

A4. RTS Inbound Rusak

Dr. Beban Kerugian Barang Rusak (64-700)  = hppTotal
    Cr. Persediaan (13-100)               = hppTotal

A5. RTS Inbound Hilang

Dr. Piutang Klaim Kurir (12-400)          = hppTotal
    Cr. Persediaan (13-100)               = hppTotal

A6. Klaim Diterima

Dr. Kas (11-100)                          = amount
    Cr. Piutang Klaim Kurir (12-400)      = amount

A7. Klaim Ditolak

Dr. Beban Kerugian Piutang (64-600)       = amount
    Cr. Piutang Klaim Kurir (12-400)      = amount

A8. PO Receive — Cash

Dr. Persediaan (13-100)     = total
    Cr. Kas (11-100)        = total

A9. PO Receive — Utang

Dr. Persediaan (13-100)     = total
    Cr. Hutang Usaha (20-100) = total

A10. PO Payment

Dr. Hutang Usaha (20-100)   = amount
    Cr. Kas (11-100)        = amount

A11. PO Return — Cash

Dr. Kas (11-100)            = total
    Cr. Persediaan (13-100) = total

A12. PO Return — Utang

Dr. Hutang Usaha (20-100)   = total
    Cr. Persediaan (13-100) = total

A13. Standalone RTS

Shopee/TikTok: Dr. Retur = GROSS, Cr. Piutang = NET, Cr. Fees
Mengantar:     Dr. Retur = GROSS, Cr. Piutang = GROSS (tanpa fee)

A14. Closing Period (3 jurnal)

1. Dr. Pendapatan / Cr. Ikhtisar Laba Rugi (30-400)
2. Dr. Ikhtisar Laba Rugi / Cr. Beban
3. Dr/Cr Ikhtisar ↔ Laba Tahun Berjalan (30-500)

Bonus Features

Aged Receivables Phase 2

Per toko, grouped by umur piutang:
  0-7 hari:   normal (baru kirim)
  8-14 hari:  perhatian (harusnya mulai cair)
  15-30 hari: WARNING
  > 30 hari:  BAHAYA

Recurring Expenses Phase 2

User setup biaya tetap sekali → auto-jurnal setiap bulan via Oban.
"Sewa Gudang Rp 5jt setiap tanggal 1" → auto Dr. Beban / Cr. Kas

Financial Health Dashboard Phase 2

7 metrics:
1. Piutang Ratio     (piutang/omzet %)
2. Cash Gap          (masuk - keluar)
3. Days to Cash      (avg ship → settlement)
4. Burn Rate         (biaya tetap/bulan)
5. RTS Rate          (rts/total order %)
6. Margin per Toko   ((pendapatan-beban)/pendapatan %)
7. PKP Threshold     (omzet YTD / 4.8M %)

Domain 3: Order Intake

Definisi: Bagaimana order MASUK ke sistem (dari luar → production).

XLSX Import — Auto (Tanpa Staging)

Designed Phase 1

Keputusan: Langsung masuk production. TANPA preview/staging. Nelly upload 300+ order/hari. Preview = buang waktu. Duplikat auto-skip.

Flow

1. Klik "Upload XLSX" di header Pesanan
2. Modal: pilih file + pilih toko (dropdown)
3. System parse XLSX:
   a. Detect format (Shopee/TikTok/Mengantar) by column headers
   b. Extract orders + items
   c. Per item: match SKU ke products (exact match)
4. Auto-import:
   ├── Order baru → create (status: "dibuat")
   ├── Duplikat (nomor_pesanan exists) → skip
   ├── SKU unknown → order masuk, item flagged
   └── Invalid data → reject, log error
5. Toast: "285 pesanan masuk, 10 duplikat, 5 SKU unknown"

3 Platform Parsers

PlatformFormatRevenue Formula
TikTok63 kolom, multi-row per orderSKU Subtotal - Seller Discount
Shopee49 kolom, multi-row per orderTotal Pembeli + Diskon Shopee - Voucher - Paket Diskon
MengantarHTML-disguised .xlsCOD: productValue - estimatedPrice
Non-COD: productValue
JANGAN: TikTok sub_total - seller_discount (double subtraction bug!)

API Sync — Staging Pipeline

Designed Phase 2

API Sync (TikTok/Shopee/Mengantar)
  → Parser → staging_orders (upsert, dedup by platform_order_id)
  → User preview di /app/staging
  → Approve → production orders
  → Reject → skip
Kenapa staging untuk API tapi tidak untuk XLSX? API = continuous, bisa jalan otomatis di background. User perlu review sebelum masuk. XLSX = user-triggered, user sudah tau apa yang di-upload.

SKU Matching & Importer

Designed Phase 1

SKU Matching

Cascade:
1. Exact SKU match → link product_id
2. Not found → item.product_id = nil, flagged "SKU unknown"

TIDAK ADA fuzzy matching di Phase 1.
90% kasus = exact match (seller SKU konsisten).

SKU Importer (Onboarding Produk Cepat)

1. Upload XLSX pesanan 7 hari terakhir
2. System parse → extract SKU unik + nama produk
3. Tabel: SKU | Nama | HPP (user isi) | Harga Jual (user isi)
4. User bulk-fill
5. Confirm → create semua produk (stok = 0)
6. Stok masuk nanti lewat PO

Domain 4: Fulfillment

Definisi: Bagaimana order DIPROSES dari terima sampai keluar gudang.

Phase 1 — Keep Current + Enhance

Phase 1

Current flow (KEEP):
  Packing Session → scan barcode → ship_order()
  ship_order = atomic: status + stok FEFO + jurnal + audit

Enhancement Phase 1:
  + reserved_stock saat order masuk
  + courier_mapping untuk normalize ekspedisi
  + release_reservation saat ship atau cancel
Kenapa gak langsung full fulfillment? Nelly masih pindah dari v1. Ship flow sekarang WORKS. Full fulfillment = 12 tabel baru + redesign packing. Risiko delay migrasi terlalu tinggi.

Phase 2 — Full Fulfillment Flow

Phase 2

Outbound Flow

Order masuk → reserve stock
  ↓
FulfillmentOrder (batch orders → 1 dokumen)
  → generate PickList (group by Area + SKU)
  ↓
Kepala Gudang approve (TTD digital)
  ↓
Picker ambil barang → confirm qty_picked
  ↓
Packer scan → verify items → tempel resi → pack
  → TRACK SIAPA YANG PACK (PIC packer)
  ↓
Sort by ekspedisi (via courier_mapping)
  ↓
Courier Handover (manifest TTD staff + driver)
  ↓
Side effects: stock deduction + journal + audit

Inbound Flow — GRN (replace PO Receive)

PO Created → Supplier deliver ke gudang
  ↓
Goods Receipt Note (GRN):
  → Staff cek + hitung fisik
  → Input: qty_received, qty_rejected per item
  → TTD staff gudang + TTD sopir
  ↓
Stock In:
  → Lot created (FEFO: cost + expiry)
  → Journal: Dr. Persediaan / Cr. Hutang atau Kas

12 New Tables

TablePurpose
fulfillment_ordersBatch document (FO2603-00012)
fulfillment_order_itemsOrders in batch
pick_listsPick instruction (PL2603-00012)
pick_list_itemsSKUs to pick, grouped by area
shipmentsPer-order ship record (SHP-260331-00412)
courier_handoversManifest per courier (HO-260331-JNT-001)
packing_incidentsQC: wrong item, wrong qty, damaged
goods_receipt_notesEnhanced PO receive (GRN-260331-001)
grn_itemsOrdered vs received vs rejected

Domain 5: Returns

Implemented

Customer Return / RTS

Order dikirim → detect RTS → return_record
  ↓
RTS Inbound Session:
  ├── Baik   → stok masuk + lot restore + reverse piutang (A3)
  ├── Rusak  → jurnal kerugian (A4)
  └── Hilang → klaim kurir (A5)
  ↓
Klaim Kurir:
  ├── Diterima → A6: Dr. Kas / Cr. Piutang Klaim
  └── Ditolak  → A7: Dr. Beban Kerugian / Cr. Piutang Klaim

Supplier Return

├── Cash PO  → A11: Dr. Kas / Cr. Persediaan
└── Utang PO → A12: Dr. Hutang Usaha / Cr. Persediaan
v2 improvement vs v1: Lot restore saat RTS baik ✓ (v1 tidak restore lot)

Domain 6: Intelligence

Phase 3

Definisi: Data analytics & prediction. Implement setelah core stable.
FeatureApa
Demand SnapshotsDaily per SKU per warehouse, source breakdown per platform
ABC-XYZA=top 80% revenue, X=stable demand → AX=auto-reorder
Spike Detection> 2× 14-day MA → flag + auto-response
Campaign PlanningEstimated volume → trigger PO, post-campaign accuracy
Reorder SuggestionROP = daily_demand × lead_time + safety_stock
New Product TrackingSell-through benchmark → signal hot/normal/slow

Platform Differences (KRITIS)

JANGAN pernah asumsi ketiga platform identik.
Shopee & TikTokMengantar
ModelMarketplace escrowCOD offline
Fee% dari GROSSFlat (COD fee inc VAT)
PiutangNET (GROSS - fees)NET (sudah potong ongkir)
RTS ongkirPlatform absorbSeller TIDAK kena lagi
PaymentSelalu "unpaid" (escrow)COD="paid", transfer="unpaid"

Revenue Formulas (per platform)

PlatformFormulaJANGAN
TikTokpayment.sub_totalJANGAN kurangi seller_discount (sudah included!)
ShopeeTotal Pembeli + Diskon Shopee - Voucher - Paket Diskon
Mengantar CODproductValue - estimatedPriceJANGAN pakai productValue langsung
Mengantar Non-CODproductValue
ScaleVproduct_priceJANGAN pakai gross_revenue

Execution Order

Phase 1 — Migrasi v1→v2

SessionScopeSteps
AMaster Data MigrationMigration + schema + bundle + unit conv + reservation + courier
BOrder IntakeXLSX dependency + 3 parsers + import + SKU importer
CDaily Journal + FulfillmentOban worker + backfill + reservation in ship/cancel
DUI PolishProduk form + courier page + order list enrichment
EStyle Refactor (LAST)Shopify Polaris + Stripe + Xero visual

Phase 2 — Setelah v2 Live

SessionScope
FFull Fulfillment (FO, PickList, Ship, Handover, GRN)
GAccounting Enhance (recurring, aged receivables, health)
HAPI Integration (TikTok/Shopee/Mengantar sync)
ISettlement XLSX parser

Phase 3 — Scale

SessionScope
JIntelligence (demand, ABC-XYZ, spike, campaign, reorder)

Rules for Executor

  1. BACA domain yang relevan SEBELUM koding — jangan asumsi
  2. Ikuti execution order — jangan loncat session
  3. Per step: implement → compile → test → commit
  4. Semua function return {:ok, result} atau {:error, reason}
  5. org_id di setiap query — multi-tenant enforced
  6. Repo.transaction untuk multi-step — atomic
  7. Pattern matching untuk status — gak ada if-else bertingkat
  8. Test WAJIB — setiap function punya test
  9. Logic di context, BUKAN LiveView
  10. Angka JANGAN diasumsi — tanya Hafish
  11. JANGAN rewrite dari ingatan — BACA source file dulu
  12. WIB timezone — semua datetime +07:00
  13. Advisory lock — untuk ID generation
  14. Error message bahasa user, bukan teknis
  15. SCHEMA → CONTEXT → TEST → SEED → REVIEW → LIVEVIEW
  16. JANGAN lanjut layer tanpa checkpoint review