Skip to content

Latest commit

Β 

History

393 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Devotion Logo

Devotion

Marketplace kapasitas produksi untuk UMKM konveksi

Delegate Your Overload Production


Live Demo GitHub License



Submission for ITECHNO CUP 2026 β€” Web Development

By Indonesia Emas 74 Kg


πŸ“‹ Daftar Isi


πŸ‘₯ Tim Developer

Nama Peran GitHub
Juan Kevin Utomo Project Lead @TrygerZ
Fazril Syaveral Hillaby Backend Developer @fzrilsh
Chiko Maulana Ahmad Frontend Developer @ChikoID

🎯 Tentang Proyek

Latar Belakang

Bayangkan pemilik konveksi kebanjiran order 5.000 potong, sementara kapasitasnya hanya 2.000. Yang biasanya terjadi: dia menelepon satu per satu kenalan yang nomornya masih tersimpan. Kalau semuanya penuh, sisa 3.000 potong itu dilepas, padahal di kota sebelah ada konveksi yang mesinnya menganggur minggu itu.

Dua masalah bertemu di titik yang sama. Yang kelebihan order tidak tahu siapa yang sedang kosong, dan yang sedang kosong tidak punya cara memberi tahu siapa pun. Selama pencarian mitra hanya lewat relasi pribadi, jangkauannya berhenti di batas daftar kontak.

Masalahnya bertambah karena kapasitas berubah cepat. Mitra yang minggu lalu longgar bisa penuh hari ini. Tanpa kalender ketersediaan yang bisa dilihat calon pemberi order, telepon demi telepon berakhir dengan jawaban "maaf, sedang penuh".

Solusi yang Ditawarkan

Devotion mempertemukan dua sisi yang selama ini tidak saling melihat:

  • Subkontraktor, UMKM konveksi yang punya kapasitas produksi menganggur.
  • Pemberi order, UMKM atau brand yang ordernya melebihi kapasitas sendiri.

Subkontraktor memasang profil usaha, mencantumkan produk dan mesin yang dikuasai, lalu mengisi kapasitas per minggu di kalender ketersediaan. Pemberi order mengisi kriteria yang dibutuhkan: produk apa, mesin apa, berapa banyak, kapan deadline-nya, sejauh mana wilayah yang masih masuk hitungan. Dari hasil pencarian dia memilih beberapa kandidat sekaligus dan mengirim satu request kuota ke semuanya. Offer yang diterima langsung menjadi work order, dan kapasitas yang dipakai tercatat per minggu.

Yang membedakan Devotion dari daftar kontak biasa adalah cara hasil pencarian disusun. Setiap kandidat dinilai dengan empat kriteria keras dan skor 0 sampai 4: produk cocok atau tidak, mesin cocok atau tidak, jeda kesiapan masih terjangkau atau tidak, dan kapasitas kalau dijumlah sampai deadline cukup atau tidak. Reputasi, badge verifikasi, kebaruan kalender, dan jarak sengaja tidak masuk skor. Konsekuensinya: urutan yang muncul selalu bisa dijelaskan alasannya, dan pencarian yang sama menghasilkan urutan yang sama.

Tujuan Proyek

  • 🎯 Tujuan Utama: membuat kapasitas produksi yang menganggur bisa ditemukan lewat proses terbuka, bukan lewat kenalan.
  • πŸ“Š Target Pengguna: pemilik konveksi yang mau menerima subkontrak, pemilik brand atau UMKM yang perlu melimpahkan order, dan admin yang menjaga operasional platform.
  • πŸ’‘ Value Proposition: satu tempat untuk memasang, mencari, meminta, dan memantau kapasitas, dengan data yang sama dilihat kedua pihak. Tidak ada lagi versi kebenaran yang berbeda antara pemberi order dan subkontraktor.
  • 🌐 Kaitan Tema dan SDG: kalender kapasitas membuat informasi produksi ikut bergerak setiap kali ketersediaan berubah, dan di situlah sifat adaptifnya. Devotion menyentuh SDG 8 lewat akses order dan pemanfaatan kapasitas kerja yang tadinya terbuang, serta SDG 9 lewat digitalisasi proses matching antar pelaku industri kecil.

Batasan Produk

Devotion tidak menyentuh uang siapa pun. Tidak menahan, tidak menyalurkan, tidak memproses. Pembayaran terjadi langsung antar pihak, dan platform hanya mencatat pernyataan keduanya bahwa pembayaran sudah dikirim atau diterima. Tidak ada kolom nominal sama sekali di catatan itu, karena begitu ada, platform mulai terlihat seperti perantara dana.

Antarmuka seluruhnya bahasa Indonesia, tanpa layer i18n. Sasarannya UMKM konveksi domestik, jadi menambah mekanisme multi-bahasa hanya menambah kerumitan tanpa ada yang memakainya.


✨ Fitur Unggulan

Fitur Utama

Fitur Deskripsi Keunggulan
Profil dan autentikasi usaha Registrasi dengan pilihan peran, login, session berbasis cookie httpOnly, verifikasi email dan nomor HP, password recovery, profil usaha dengan titik lokasi di peta. Sebelum ada uang dan deadline yang dipertaruhkan, kedua pihak sudah tahu sedang berhadapan dengan siapa.
Listing kapasitas dan kalender mingguan Subkontraktor mengatur kapasitas mingguan, jeda kesiapan, jenis produk, jenis mesin, visibilitas listing, dan periode ketersediaan 12 minggu ke depan. Minggu yang sudah punya alokasi terkunci dari perubahan. Kapasitas kosong akhirnya terlihat tanpa perlu dikenal lebih dulu, dan kalender tidak bisa berubah di belakang pesanan yang sedang berjalan.
Pencarian dan skor kecocokan Filter produk, mesin, jumlah, deadline, jeda maksimum, dan cakupan kota, provinsi, atau nasional. Skor 0 sampai 4 dari kriteria keras, kapasitas dijumlah lintas periode sampai deadline, pagination cursor dengan urutan deterministik. Pengguna melihat kriteria mana yang tidak terpenuhi, bukan sekadar angka tanpa penjelasan.
Request kuota multi-kandidat dan offer Satu request dikirim ke beberapa listing. Tiap kandidat punya status sendiri dan batas balasan 72 jam. Subkontraktor mengirim offer, pemberi order dapat counter-offer, rantai offer tercatat per babak. Menawar ke lima calon mitra tidak lagi berarti lima percakapan terpisah yang harus diingat sendiri.
Work order dengan alokasi kapasitas Offer yang diterima menjadi work order. Alokasi kapasitas ditulis dalam satu transaction dengan row lock terurut menurut minggu. Kapasitas yang sudah dijanjikan tidak pernah terjual dua kali, bahkan ketika dua pemberi order menekan tombol terima pada detik yang sama.
State machine pesanan dan auto-confirm Tujuh status pesanan dengan transisi yang dikirim backend lewat allowed_transitions. Pesanan berstatus dikirim dianggap diterima otomatis setelah 7 hari, dengan reminder 2 hari sebelumnya, dan berhenti bila ada sengketa. Pesanan punya batas waktu sendiri, jadi subkontraktor tidak tersangkut menunggu konfirmasi yang tak pernah datang.
Pembatalan, sengketa, dan mediasi admin Pembatalan pra-produksi membalik seluruh baris alokasi. Sengketa menghentikan auto-confirm dan masuk ke antrean mediasi admin dengan hasil dilanjutkan, dikonfirmasi selesai, atau dibatalkan. Kalau kesepakatan gagal sebelum produksi, kapasitasnya kembali bisa dijual. Kalau berselisih, ada jalur resmi.
Reputasi dan completion rate Review setelah pesanan selesai, nilai reputasi turunan, dan completion rate yang hanya ditampilkan sebagai persentase bila datanya cukup. Pembatalan membebani pihak yang membatalkan. Usaha baru tidak dihukum angka reputasi yang dihitung dari dua transaksi.
Panel admin Antrean verifikasi identitas, keputusan usulan item, pengelolaan master data, pesanan telat, sengketa, moderasi review, dan status koneksi WhatsApp beserta QR untuk menyambung ulang. Semua urusan operasional bisa diselesaikan dari antarmuka, tanpa sekali pun membuka database.

Fitur Tambahan

  • Master data produk, mesin, dan wilayah menjaga istilah pencarian tetap seragam. Kalau setiap orang mengisi jenis produk dengan kata-katanya sendiri, pencarian berhenti berfungsi.
  • Usulan item baru untuk produk atau mesin yang belum ada di master data. Usulannya diputuskan admin, jadi daftar tetap rapi tanpa menutup kebutuhan yang belum terdaftar.
  • Verifikasi identitas usaha lewat upload dokumen dan foto lokasi. Tipe file diperiksa dari magic bytes, bukan dari header yang bisa dipalsukan. Nama file dibuat sistem, metadata lokasi pada gambar dibuang, dan file hanya bisa diakses pemiliknya serta admin.
  • Notifikasi in-app dengan penanda sudah dibaca, jumlah belum dibaca, dan pilihan channel email atau WhatsApp. Notifikasi transaksional tidak bisa dimatikan.
  • Rate limiting berbasis data domain untuk percobaan login, kode verifikasi per nomor dan per alamat asal, serta request kuota per pengguna. Penegakannya di aplikasi, tidak diserahkan ke edge proxy.
  • Health check untuk database, koneksi WhatsApp, dan ruang penyimpanan. Endpoint yang sama dipakai sebagai healthcheck container.
  • Error response konsisten dalam format application/problem+json (RFC 9457), dengan 34 error code stabil dan detail bahasa Indonesia yang bisa dikutip penguji langsung ke laporan.
  • Swagger UI di /docs saat mode development, membaca kontrak OpenAPI yang sama dengan yang di-embed ke binary.

πŸ“Έ Demo & Screenshot

Live Demo

πŸ”— https://devotion.web.id/

Screenshot Aplikasi

link gist : devotion-screenshots

Beranda
Beranda
Hasil pencarian dengan skor kecocokan
Hasil pencarian dengan skor kecocokan
Kalender kapasitas mingguan
Kalender kapasitas mingguan
Dasbor admin
Dasbor admin
Halaman publik (6 page)

Bisa dibuka tanpa login. Beranda tidak diulang di sini karena sudah tampil di sorotan atas.

Tentang
Tentang
Profil usaha publik
Profil usaha publik
Bantuan
Bantuan
Syarat dan ketentuan
Syarat dan ketentuan
Kebijakan privasi
Kebijakan privasi
Halaman tidak ditemukan
Halaman tidak ditemukan
Autentikasi dan verifikasi (6 page)

Registrasi sampai pemulihan kata sandi.

Registrasi
Registrasi
Login
Login
Verifikasi email
Verifikasi email
Verifikasi nomor HP
Verifikasi nomor HP
Lupa kata sandi
Lupa kata sandi
Atur ulang kata sandi
Atur ulang kata sandi
Subkontraktor (10 page)

Sisi pemilik kapasitas: memasang listing, mengatur kalender, menjawab permintaan.

Profil usaha saya
Profil usaha saya
Verifikasi identitas usaha
Verifikasi identitas usaha
Listing kapasitas
Listing kapasitas
Kalender kapasitas mingguan
Kalender kapasitas mingguan
Permintaan kuota masuk
Permintaan kuota masuk
Detail permintaan dan penawaran
Detail permintaan dan penawaran
Daftar pesanan
Daftar pesanan
Detail pesanan, sisi subkontraktor
Detail pesanan, sisi subkontraktor
Notifikasi
Notifikasi
Preferensi notifikasi
Preferensi notifikasi
Pemberi order (8 page)

Sisi pencari kapasitas: mencari, meminta kuota, memantau pesanan.

Form pencarian kapasitas
Form pencarian kapasitas
Hasil pencarian dengan skor kecocokan
Hasil pencarian dengan skor kecocokan
Buat permintaan kuota multi-kandidat
Buat permintaan kuota multi-kandidat
Permintaan terkirim
Permintaan terkirim
Detail permintaan dan perbandingan penawaran
Detail permintaan dan perbandingan penawaran
Daftar pesanan
Daftar pesanan
Detail pesanan, sisi pemberi order
Detail pesanan, sisi pemberi order
Profil usaha saya
Profil usaha saya
Panel admin (10 page)

Operasional platform, seluruhnya lewat antarmuka tanpa menyentuh database.

Dasbor admin
Dasbor admin
Antrean verifikasi identitas
Antrean verifikasi identitas
Daftar baku produk dan mesin
Daftar baku produk dan mesin
Usulan item baru
Usulan item baru
Pesanan telat
Pesanan telat
Detail pesanan, sisi admin
Detail pesanan, sisi admin
Sengketa dan mediasi
Sengketa dan mediasi
Moderasi ulasan
Moderasi ulasan
Status sambungan WhatsApp
Status sambungan WhatsApp
Kesehatan sistem
Kesehatan sistem

Video Demo

πŸ“Ή Link video demo: https://www.youtube.com/watch?v=u7FIAiUQOng


πŸ› οΈ Teknologi

Tech Stack

Frontend

Framework    : React 18.3.1, TypeScript 5.8
Build tool   : Vite 8.2.0, hasil build di-embed ke binary Go
UI Library   : Tailwind CSS 4.3.3
State Mgmt   : TanStack Query 5.102.1
Validation   : React Hook Form 7.86.0 + Zod 4.4.3
Routing      : React Router 7.18.2
Peta         : Leaflet 1.9.4 + tile OpenStreetMap
API client   : Fetch API, tipe di-generate dari OpenAPI

Backend

Runtime      : Go 1.25.0
Framework    : net/http, router bawaan Go 1.22+
Database     : PostgreSQL 16, pgx/v5 5.7.5 + query hasil generate sqlc
Migration    : golang-migrate, otomatis saat startup di bawah advisory lock
Auth         : bcrypt, session cookie httpOnly, token disimpan sebagai hash
Notifikasi   : net/smtp ke Mailjet, whatsmeow untuk WhatsApp
Observability: log/slog JSON dengan request ID, Sentry opsional

DevOps & Tools

Deployment   : Docker Compose, tepat 2 service (backend, postgres)
CI/CD        : GitHub Actions, image ke GitHub Container Registry
Edge dan TLS : Cloudflare Origin Certificate, TLS diselesaikan binary Go
Testing      : go vet, go test, Jest, ESLint, tsc

Alasan Pemilihan Teknologi

Satu batasan menentukan hampir semua pilihan: aturan panitia yang membatasi service runtime menjadi dua.

Teknologi Alasan Pemilihan
React + Vite Hasil build di-embed ke binary Go lewat embed.FS, sehingga frontend menjadi file statis, bukan service runtime ketiga.
Go + net/http Satu binary, router sudah ada di standard library sejak Go 1.22, jejak memori kecil.
PostgreSQL 16 Alokasi kapasitas butuh transaction, CHECK constraint, dan row lock. Dua kesepakatan bersamaan diselesaikan database, bukan logika aplikasi.
sqlc, bukan ORM Query pencarian dan skor kecocokan adalah inti produk. SQL ditulis eksplisit agar dapat dibaca dan diaudit.
OpenAPI + openapi-typescript Kontrak menjadi sumber tipe frontend. Perubahan bentuk response memunculkan compile error, bukan runtime bug.
Tailwind CSS Cukup untuk antarmuka mobile-first tanpa menambah component library kedua.
Leaflet + OpenStreetMap Peta tanpa API key dan tanpa tagihan. Jarak bersifat informatif dan tidak memengaruhi skor.

Dependencies Utama

{
  "dependencies": {
    "react": "^18.3.1",
    "@tanstack/react-query": "^5.102.1",
    "react-hook-form": "^7.86.0",
    "zod": "^4.4.3",
    "react-router-dom": "^7.18.2",
    "tailwindcss": "^4.3.3",
    "leaflet": "^1.9.4"
  }
}
Backend  github.com/jackc/pgx/v5 v5.7.5
         github.com/golang-migrate/migrate/v4 v4.18.3
         golang.org/x/crypto v0.54.0
         golang.org/x/term v0.45.0
         google.golang.org/protobuf v1.36.11
         go.mau.fi/whatsmeow
         github.com/getsentry/sentry-go v0.35.3

Daftar backend pendek karena disengaja. Structured logging (log/slog), email (net/smtp), pembuangan metadata gambar (image/jpeg), token acak (crypto/rand), UUID (gen_random_uuid()), dan jarak haversine semuanya diselesaikan standard library.


πŸ—οΈ Arsitektur Sistem

System Architecture

Satu binary Go melayani API JSON dan React SPA dari proses yang sama. Cloudflare, Mailjet, WhatsApp, dan Sentry berstatus dependensi eksternal, bukan container.

flowchart TB
    subgraph client["Klien"]
        browser["Browser<br/>React 18 SPA<br/>cookie httpOnly"]
    end

    subgraph edge["Edge"]
        cf["Cloudflare<br/>proxy dan TLS<br/>Origin Certificate"]
    end

    subgraph host["Server, docker compose"]
        subgraph be["Layanan 1: backend, Go 1.25"]
            spa["Static handler<br/>embed.FS webdist"]
            api["API handler<br/>net/http router"]
            gate["Session dan role gate<br/>UncoveredAPIRoutes fail fast"]
            sched["Scheduler in process<br/>time.Ticker, advisory lock"]
            mig["Migrator<br/>golang-migrate<br/>pg_try_advisory_lock"]
        end
        pg[("Layanan 2: postgres 16<br/>max_connections 20")]
    end

    subgraph ext["Layanan eksternal, bukan container"]
        mail["Mailjet<br/>net/smtp"]
        wa["WhatsApp<br/>whatsmeow"]
        sentry["Sentry<br/>error tracking"]
    end

    browser -->|HTTPS| cf
    cf -->|"origin TLS, rentang IP dipatok"| spa
    cf -->|"/api"| api
    api --> gate
    gate -->|"pgx pool, maksimal 15 koneksi"| pg
    spa -.->|"non /api, SPA fallback"| browser
    mig -->|"saat startup"| pg
    sched -->|"tenggat dan antrean notifikasi"| pg
    sched -.-> mail
    sched -.-> wa
    api -.-> sentry

    classDef svc fill:#1f6feb,stroke:#0d419d,color:#ffffff
    classDef db fill:#238636,stroke:#196c2e,color:#ffffff
    classDef extn fill:#6e7681,stroke:#484f58,color:#ffffff
    class spa,api,gate,sched,mig svc
    class pg db
    class mail,wa,sentry extn
Loading
Properti Penerapan Konsekuensi
Satu proses, dua peran Static handler membaca embed.FS; path selain /api jatuh ke SPA fallback, /api tak dikenal membalas 404 JSON. Frontend bukan service runtime, dan salah tulis endpoint tetap menghasilkan error JSON.
Role gate wajib Setiap pola /api terdaftar publik atau bergerbang; UncoveredAPIRoutes() diperiksa saat serve. Proses menolak menyala bila ada pola tanpa keputusan peran.
Scheduler in-process time.Ticker di binary yang sama, tiap job dibungkus advisory lock, deadline juga dievaluasi saat data dibaca. Tanpa worker terpisah, dan job tidak dieksekusi ganda.
Migrasi terkunci golang-migrate saat startup di bawah pg_try_advisory_lock. Deployment tanpa langkah migrasi manual, dua instance tidak saling menimpa schema.

Database Schema

26 tabel domain, plus schema_migrations dan tabel milik whatsmeow. ERD dipecah per konteks. Atribut dibatasi pada key dan kolom yang menentukan perilaku; created_at dan updated_at tidak diulang.

Identitas, wilayah, dan verifikasi (7 tabel)
erDiagram
    province ||--o{ city : "membawahi"
    city ||--o{ business_profile : "melokasikan"
    user_account ||--o| business_profile : "memiliki"
    user_account ||--o{ session : "membuka"
    user_account ||--o{ verification_code : "menerima"
    business_profile ||--o{ uploaded_file : "mengunggah"
    business_profile ||--o{ verification_request : "mengajukan"
    uploaded_file ||--o{ verification_request : "melampirkan"

    province {
        text code PK "regex 2 digit"
        text name
    }
    city {
        text code PK "regex 4 digit"
        text province_code FK "2 digit awal sama"
        text name
    }
    user_account {
        uuid id PK
        citext email UK
        text phone UK "regex 62 diikuti 8 sampai 13 digit"
        text password_hash "bcrypt"
        boolean email_verified
        boolean phone_verified
        boolean role_subcontractor
        boolean role_buyer
        boolean role_admin "eksklusif dari peran usaha"
        boolean notif_nontx_email "preferensi kanal non transaksional"
        boolean notif_nontx_whatsapp "preferensi kanal non transaksional"
    }
    business_profile {
        uuid id PK
        uuid account_id FK,UK
        text business_name "minimal 3 karakter"
        text city_code FK
        numeric latitude "dibatasi wilayah Indonesia"
        numeric longitude "wajib berpasangan dengan latitude"
        text description
        boolean verified
    }
    session {
        uuid id PK
        uuid account_id FK
        bytea token_hash UK "hash, bukan token mentah"
        inet source_address
        timestamptz expires_at
        timestamptz accessed_at "dasar perpanjangan sesi"
    }
    verification_code {
        uuid id PK
        uuid account_id FK
        verification_purpose purpose "email, phone, recovery"
        bytea code_hash
        timestamptz expires_at
        timestamptz consumed_at
    }
    uploaded_file {
        uuid id PK
        uuid owner_profile_id FK
        file_type type "identity_document, location_photo"
        text original_name
        text mime_type "jpeg, png, pdf saja"
        integer size_bytes "maksimal 5 MB"
        text storage_path UK "nama dibuat sistem"
    }
    verification_request {
        uuid id PK
        uuid profile_id FK "satu pengajuan pending per profil"
        text identity_number
        uuid identity_file_id FK
        uuid location_file_id FK
        verification_status status
        text admin_note "wajib bila ditolak"
        uuid decided_by FK "user_account, wajib bila sudah diputus"
        timestamptz decided_at
        inet applicant_source_address
    }
Loading
Katalog, listing, dan kalender kapasitas (6 tabel)
erDiagram
    business_profile ||--o| capacity_listing : "menerbitkan"
    business_profile ||--o{ item_proposal : "mengusulkan"
    capacity_listing ||--o{ availability_period : "menjadwalkan"
    capacity_listing ||--o{ listing_product : "menawarkan"
    capacity_listing ||--o{ listing_machine : "mengoperasikan"
    catalog_item ||--o{ listing_product : "dirujuk"
    catalog_item ||--o{ listing_machine : "dirujuk"
    catalog_item ||--o{ item_proposal : "dihasilkan"

    catalog_item {
        uuid id PK
        item_type type "product atau machine"
        text name "unik per tipe"
        boolean active
        integer sort_order
    }
    item_proposal {
        uuid id PK
        uuid profile_id FK
        item_type type
        text proposed_name
        proposal_status status
        text admin_note
        uuid item_id FK "terisi bila disetujui"
        uuid decided_by FK "user_account"
        timestamptz decided_at
    }
    capacity_listing {
        uuid id PK
        uuid profile_id FK,UK "satu listing per profil"
        integer weekly_capacity "harus positif"
        integer readiness_lead_days "0 sampai 365"
        boolean published
        timestamptz calendar_updated_at "dasar penanda kalender basi"
        date horizon_until "wajib hari Senin"
        timestamptz stale_notified_at "penanda pengingat kalender basi, direset saat kalender diperbarui"
    }
    availability_period {
        uuid id PK
        uuid listing_id FK
        date week_start "wajib hari Senin, unik per listing"
        integer total_capacity
        integer used_capacity "tidak melebihi total"
        boolean marked_full
    }
    listing_product {
        uuid listing_id PK,FK
        uuid item_id PK,FK
    }
    listing_machine {
        uuid listing_id PK,FK
        uuid item_id PK,FK
        integer machine_count "harus positif"
    }
Loading
Request kuota, offer, dan work order (10 tabel)
erDiagram
    business_profile ||--o{ quota_request : "mengirim"
    business_profile ||--o{ request_candidate : "dijangkau sebagai subkontraktor"
    business_profile ||--o{ work_order : "menjadi pembeli atau subkontraktor"
    business_profile ||--o{ payment_record : "menyatakan pembayaran"
    business_profile ||--o{ review : "menulis dan menerima"
    business_profile ||--o{ dispute : "melaporkan"
    catalog_item ||--o{ quota_request : "menentukan produk"
    quota_request ||--o{ request_candidate : "menjangkau"
    capacity_listing ||--o{ request_candidate : "dinilai"
    request_candidate ||--o{ offer : "menampung"
    request_candidate ||--o| work_order : "menghasilkan"
    offer ||--o| work_order : "mendasari"
    work_order ||--o{ capacity_allocation : "memotong"
    availability_period ||--o{ capacity_allocation : "dipotong"
    work_order ||--o{ work_order_status_history : "mencatat"
    work_order ||--o{ payment_record : "mencatat pernyataan"
    work_order ||--o| dispute : "menimbulkan"
    work_order ||--o{ review : "membuka"

    quota_request {
        uuid id PK
        uuid buyer_id FK
        uuid product_item_id FK
        integer quantity "harus positif"
        text material
        date deadline
        text note
        timestamptz reply_due_at "72 jam sejak dibuat"
    }
    request_candidate {
        uuid id PK
        uuid request_id FK "satu kandidat per listing per request"
        uuid listing_id FK
        uuid subcontractor_id FK
        candidate_status status "6 nilai, hanya satu agreed per request"
        text rejection_reason
    }
    offer {
        uuid id PK
        uuid candidate_id FK
        integer sequence "unik per kandidat, satu babak penawaran"
        offer_party proposed_by "subcontractor atau buyer"
        bigint total_price "rupiah bulat, harus positif"
        integer readiness_lead_days
        text note
    }
    work_order {
        uuid id PK
        uuid candidate_id FK,UK
        uuid offer_id FK
        uuid buyer_id FK "wajib berbeda dari subkontraktor"
        uuid subcontractor_id FK
        integer quantity
        bigint total_price "rupiah bulat"
        date deadline
        date readiness_week_start "wajib hari Senin, tidak melewati deadline"
        work_order_status status "7 status"
        timestamptz shipped_at
        timestamptz auto_confirm_base_at "dasar hitung 7 hari"
        timestamptz confirmed_at
        boolean auto_confirmed
        timestamptz confirm_warn_sent_at "penanda pengingat sekali kirim"
        timestamptz late_notified_at "penanda pemberitahuan lewat tenggat"
        timestamptz deadline_warn_sent_at "penanda peringatan tenggat pengiriman mendekat"
        uuid cancelled_by_id FK
        text cancellation_reason
        timestamptz cancelled_at
    }
    capacity_allocation {
        uuid id PK
        uuid work_order_id FK
        uuid period_id FK "satu baris per pasangan order dan periode"
        integer quantity "harus positif"
        timestamptz reversed_at "terisi saat alokasi dibalik"
    }
    work_order_status_history {
        uuid id PK
        uuid work_order_id FK
        work_order_status old_status
        work_order_status new_status
        uuid changed_by FK "user_account, wajib bila bukan sistem"
        boolean by_system
        text note
    }
    payment_record {
        uuid id PK
        uuid work_order_id FK
        uuid profile_id FK
        payment_direction direction "sent atau received"
        date date "tanpa kolom jumlah uang"
        text note
    }
    dispute {
        uuid id PK
        uuid work_order_id FK "satu sengketa terbuka per pesanan"
        uuid reporter_id FK
        text report_body
        dispute_status status
        dispute_result result "cancelled, continued, confirmed"
        boolean allocation_reversed
        uuid liable_party_id FK
        text admin_note
        uuid handled_by FK "user_account"
        timestamptz resolved_at
    }
    review {
        uuid id PK
        uuid work_order_id FK
        uuid reviewer_id FK "tidak boleh sama dengan reviewee"
        uuid reviewee_id FK
        smallint rating "1 sampai 5"
        text text
        boolean hidden
        uuid hidden_by FK "user_account"
        timestamptz hidden_at
        text hidden_reason
    }
Loading

Tiga aturan harus membaca tabel lain, jadi ditegakkan trigger bukan CHECK: trg_reject_self_request (kandidat tidak boleh sama dengan pembeli), trg_reject_allocation_before_readiness (alokasi tidak boleh sebelum readiness_week_start), dan trg_reject_wrong_product_item / trg_reject_wrong_machine_item (setiap baris terikat ke tipe item yang benar).

Notifikasi dan rate limiting (3 tabel)
erDiagram
    user_account ||--o{ notification : "menerima"
    notification ||--o{ notification_channel : "dikirim lewat"

    notification {
        uuid id PK
        uuid account_id FK
        event_type event "16 jenis kejadian"
        boolean transactional "tidak dapat dimatikan pengguna"
        text title
        text body
        text link
        timestamptz read_at
    }
    notification_channel {
        uuid id PK
        uuid notification_id FK "satu baris per kanal"
        notification_channel_type channel "email atau whatsapp"
        delivery_status status "pending, sent, failed_permanent"
        smallint attempts "maksimal 3"
        text last_error
        timestamptz attempted_at "urutan antrean pengiriman"
        timestamptz sent_at
    }
    rate_limit {
        uuid id PK
        rate_limit_target target "login, otp per nomor, otp per alamat, request kuota"
        text key
        timestamptz window_start "unik bersama target dan key"
        integer count
    }
Loading

rate_limit tanpa foreign key. key menyimpan pengenal sasaran sebagai teks (id akun, nomor, atau alamat asal) sesuai target, sehingga pembatasan tetap berlaku bagi pihak yang belum punya akun.

Keputusan Schema

Keputusan Penerapan Alasan
Uang bilangan bulat offer.total_price dan work_order.total_price bigint, CHECK (total_price > 0). Rupiah tidak dipecah di B2B ini, dan tipe pecahan menimbulkan rounding error.
Minggu selalu Senin week_start, horizon_until, readiness_week_start date dengan CHECK (EXTRACT(ISODOW ...) = 1). Periode tidak jatuh di tengah minggu, jadi kapasitas tidak berkurang dari periode salah.
Platform tidak memegang dana payment_record tanpa kolom nominal, hanya arah dan tanggal, unik per pesanan, pihak, dan arah. Platform mencatat pernyataan kedua pihak tanpa jadi perantara dana.
Kapasitas tidak terjual dua kali capacity_allocation unik per work_order_id dan period_id, used_capacity <= total_capacity, satu transaction dengan SELECT ... FOR UPDATE terurut week_start. Batas ditegakkan database, dan urutan lock seragam mencegah deadlock.
Audit trail lengkap dispute, item_proposal, verification_request, review memakai CHECK gabungan: kolom pendukung wajib terisi begitu status keluar dari pending. Status terminal tidak tersimpan tanpa catatan admin, waktu, dan pelaku.
Token tidak disimpan mentah session.token_hash dan verification_code.code_hash bytea berisi hash. Kebocoran isi tabel tidak langsung berarti session hijacking.

Definisi lengkap beserta index dan constraint ada di data-model.md dan backend/db/migrations/.

Folder Structure

devotion/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ cmd/devotion/       # serve, admin:create, seed:*, reset:*, user:verify, health:check
β”‚   β”œβ”€β”€ apidocs/            # Swagger UI dan salinan openapi.yaml yang di-embed
β”‚   β”œβ”€β”€ internal/
β”‚   β”‚   β”œβ”€β”€ platform/       # clock, config, httpx, session, storage, scheduler,
β”‚   β”‚   β”‚                   # ratelimit, cloudflare, health, migrate, observability, tlsconf
β”‚   β”‚   β”œβ”€β”€ account/        # akun, peran, profil, autentikasi, recovery
β”‚   β”‚   β”œβ”€β”€ verification/   # upload file, pengajuan, keputusan admin
β”‚   β”‚   β”œβ”€β”€ masterdata/     # master data, wilayah, usulan item
β”‚   β”‚   β”œβ”€β”€ listing/        # listing kapasitas, kalender ketersediaan
β”‚   β”‚   β”œβ”€β”€ search/         # kriteria keras, skor, tie-breaker, keyset pagination
β”‚   β”‚   β”œβ”€β”€ quota/          # request kuota, offer, counter-offer
β”‚   β”‚   β”œβ”€β”€ order/          # work order, alokasi, pembatalan, pembayaran, sengketa
β”‚   β”‚   β”œβ”€β”€ reputation/     # review, nilai turunan, moderasi
β”‚   β”‚   β”œβ”€β”€ notification/   # queue, pengiriman, retry
β”‚   β”‚   β”œβ”€β”€ admin/          # status dan koneksi WhatsApp
β”‚   β”‚   └── db/             # pool 15 koneksi, sqlcgen, testdb schema uji terpisah
β”‚   β”œβ”€β”€ db/migrations/      # 22 migrasi golang-migrate
β”‚   β”œβ”€β”€ db/queries/         # 15 file SQL sumber sqlc
β”‚   └── webdist/            # hasil build frontend, di-embed lewat embed.FS
β”œβ”€β”€ frontend/src/
β”‚   β”œβ”€β”€ api/                # fetch client dan tipe hasil generate dari OpenAPI
β”‚   β”œβ”€β”€ components/         # layout, section, komponen bersama
β”‚   β”œβ”€β”€ pages/              # 38 halaman, dikelompokkan per user story
β”‚   β”œβ”€β”€ hooks/              # hook TanStack Query per domain
β”‚   β”œβ”€β”€ schemas/            # schema Zod
β”‚   β”œβ”€β”€ routes/             # GuestRoute, ProtectedRoute, UnverifiedRoute
β”‚   β”œβ”€β”€ providers/          # QueryProvider
β”‚   β”œβ”€β”€ lib/                # helper murni
β”‚   β”œβ”€β”€ test/               # setup dan utilitas Jest
β”‚   └── styles/             # entri Tailwind dan style global
β”œβ”€β”€ docs/                   # spec, plan, data-model, contracts, tasks, panduan operasional
β”œβ”€β”€ .github/workflows/ci.yml
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ .env.example
β”œβ”€β”€ LICENSE
└── README.md

βš™οΈ Instalasi & Setup

Prerequisites

Pastikan Anda telah menginstall:

  • Git
  • Docker Engine dengan Compose v2
  • Go 1.25.0 atau lebih baru
  • Node.js 20 atau lebih baru

Langkah Instalasi

1️⃣ Clone Repository

git clone https://github.com/fzrilsh/devotion.git
cd devotion

2️⃣ Setup Environment Variables

cp .env.example .env

Wajib di semua environment hanya empat: APP_ENV, APP_BASE_URL, DATABASE_URL, UPLOAD_PATH. Sisanya diwajibkan saat APP_ENV=production.

APP_ENV=development
APP_BASE_URL=http://localhost:8080
POSTGRES_USER=devotion
POSTGRES_PASSWORD=ganti_password_lokal
POSTGRES_DB=devotion
DATABASE_URL=postgres://devotion:***@127.0.0.1:5434/devotion?sslmode=disable
UPLOAD_PATH=/absolute/path/to/devotion/backend/uploads
UPLOAD_MAX_TOTAL_MB=500
UPLOAD_MAX_FILE_MB=5

Port 5434 bukan salah tulis: Compose menerbitkan Postgres di 127.0.0.1:5434 agar tidak bertabrakan dengan Postgres lain di 5432, dan ikatan loopback menjaganya tidak terekspos keluar mesin.

Pengaturan TLS, Mailjet, WhatsApp, dan Sentry ada di quickstart.md. File .env tidak pernah di-commit.

3️⃣ Setup Database

mkdir -p /absolute/path/to/devotion/backend/uploads
docker compose up -d postgres

Tanpa langkah migrasi manual. Migrasi jalan saat backend menyala, di bawah advisory lock.

4️⃣ Run Backend

Binary Go membaca environment shell, bukan file .env; yang membaca .env hanya Docker Compose. Tanpa export, serve berhenti dengan variabel lingkungan wajib belum diisi: APP_ENV.

set -a; . ./.env; set +a
cd backend
go run ./cmd/devotion serve

Backend mendengarkan di http://localhost:8080. Swagger UI di /docs, hanya saat APP_ENV=development.

5️⃣ Seed Data dan Buat Akun Admin

Sekali per database baru, dari backend/ dengan environment yang sama.

go run ./cmd/devotion seed:regions
go run ./cmd/devotion seed:master-data
go run ./cmd/devotion admin:create
go run ./cmd/devotion seed:test-data      # menolak jalan bila APP_ENV=production

Data wilayah dibaca dari salinan di repository, tanpa memanggil layanan luar.

6️⃣ Run Development Server

cd frontend
npm install
npm run dev

Aplikasi akan berjalan di http://localhost:5173 dan memproksikan /api ke port 8080.

Proxy ini wajib: tanpanya frontend dan backend terlihat sebagai origin berbeda, cookie SameSite=Lax tidak terkirim, dan setiap request tampak belum login meski login berhasil.


πŸš€ Penggunaan

Menjalankan Aplikasi

# Development mode, backend
cd backend && go run ./cmd/devotion serve

# Development mode, frontend (terminal lain)
cd frontend && npm run dev

# Production build
npm run build

# Run tests
npm run test

# Linting
npm run lint

Untuk production, CI membangun frontend, menyalin hasilnya ke backend/webdist/, lalu membangun image. Server hanya menarik dan menjalankan, supaya proses build tidak berebut resource dengan Postgres yang sedang hidup.

User Guide

Untuk Subkontraktor

  1. Registrasi: daftar sebagai Subkontraktor atau Keduanya, selesaikan verifikasi email dan nomor HP.
  2. Profil usaha: lengkapi profil beserta titik lokasi di peta.
  3. Verifikasi identitas: ajukan untuk mendapat badge terverifikasi. Opsional, tapi menaikkan kepercayaan calon pemberi order.
  4. Buat listing: produk yang dikuasai, mesin dan jumlah unit, kapasitas per minggu, jeda kesiapan.
  5. Isi kalender: ketersediaan 12 minggu ke depan, tandai minggu yang penuh.
  6. Terbitkan listing: sebelum diterbitkan, listing tidak muncul di pencarian.
  7. Balas request kuota: kirim offer berisi harga total dan jeda kesiapan. Batas 72 jam, lewat itu dianggap tidak membalas.
  8. Jalankan pesanan: lewat status produksi, selesai, dikirim. Catat pernyataan pembayaran, buka sengketa bila ada ketidaksesuaian.

Untuk Pemberi Order

  1. Registrasi: masuk sebagai Pemberi Order atau Keduanya.
  2. Cari kapasitas: isi kriteria produk, mesin, jumlah, deadline, jeda maksimum, cakupan wilayah.
  3. Bandingkan kandidat: lihat skor 0 sampai 4 beserta kriteria mana yang tidak terpenuhi.
  4. Kirim request kuota: pilih beberapa kandidat, satu request untuk semuanya.
  5. Terima offer: bandingkan, counter-offer bila perlu, terima satu offer. Work order terbentuk dan kapasitas mitra langsung terpakai.
  6. Konfirmasi penerimaan: bila dibiarkan, pesanan dianggap diterima otomatis setelah 7 hari, dengan reminder 2 hari sebelumnya.
  7. Beri review setelah pesanan selesai.

Untuk Admin

  1. Akses Admin Panel: akun admin pertama dibuat lewat go run ./cmd/devotion admin:create, bukan pendaftaran biasa. Panel ada di /admin.
  2. Verifikasi dan master data: antrean verifikasi identitas, keputusan usulan item, pengelolaan master data produk dan mesin.
  3. Mediasi dan moderasi: pesanan telat, sengketa beserta mediasi dan penyelesaiannya, moderasi review, status koneksi WhatsApp dengan QR untuk menyambung ulang.

Nomor layanan WhatsApp tidak pernah ditampilkan di antarmuka.


πŸ“š API Documentation

Base URL

Development  : http://localhost:8080/api
Production   : ditentukan oleh APP_BASE_URL
Health check : /api/health
Swagger UI   : /docs, hanya saat APP_ENV=development

Semua request memakai session cookie httpOnly, tanpa token di body response. Path /api tak dikenal membalas 404 JSON, bukan index.html, sehingga salah tulis endpoint tetap dapat didiagnosis.

Endpoints

Kontrak memuat 67 operasi pada 59 path. Setiap pola /api wajib punya keputusan peran, publik atau bergerbang, dan serve menolak menyala bila ada satu pola tanpa keputusan itu.

Authentication

POST  /api/auth/register              POST  /api/auth/login
POST  /api/auth/verify-email          POST  /api/auth/logout
POST  /api/auth/verify-phone          POST  /api/auth/recover/request
POST  /api/auth/resend-code           POST  /api/auth/recover/confirm
GET   /api/me                         PATCH /api/me/roles
GET   /api/profile/me                 PUT   /api/profile/me
GET   /api/profile/{profileId}        GET   /api/profile/{profileId}/reviews

Files, Verification, Master Data

POST /api/files                       GET  /api/files/{fileId}
POST /api/verification                GET  /api/verification
GET  /api/master/products             GET  /api/master/machines
GET  /api/regions/provinces           GET  /api/regions/cities
POST /api/master/proposals

Listing & Capacity Calendar

GET /api/listing/me                   POST /api/listing/me
PUT /api/listing/me                   PUT  /api/listing/me/visibility
GET /api/listing/me/periods           PUT  /api/listing/me/periods

Search, Quota Request, Offer

GET  /api/search
POST /api/quota-requests              GET  /api/quota-requests
GET  /api/quota-requests/incoming     GET  /api/quota-requests/{requestId}
GET  /api/candidates/{candidateId}
POST /api/candidates/{candidateId}/offers
POST /api/candidates/{candidateId}/reject
POST /api/offers/{offerId}/counter    POST /api/offers/{offerId}/accept

Work Order

GET  /api/work-orders                 GET  /api/work-orders/{workOrderId}
GET  /api/work-orders/{workOrderId}/contacts
POST /api/work-orders/{workOrderId}/status
POST /api/work-orders/{workOrderId}/confirm
POST /api/work-orders/{workOrderId}/cancel
POST /api/work-orders/{workOrderId}/payments
POST /api/work-orders/{workOrderId}/disputes
POST /api/work-orders/{workOrderId}/reviews

Notification & Admin

GET  /api/notifications               POST /api/notifications/{notificationId}/read
GET  /api/notifications/preferences   PUT  /api/notifications/preferences
GET  /api/health

GET   /api/admin/verification         POST  /api/admin/verification/{requestId}/decision
GET   /api/admin/proposals            POST  /api/admin/proposals/{proposalId}/decision
GET   /api/admin/master/items         POST  /api/admin/master/items
PATCH /api/admin/master/items/{itemId}
GET   /api/admin/late-orders          GET   /api/admin/disputes
POST  /api/admin/disputes/{disputeId}/mediate
POST  /api/admin/disputes/{disputeId}/resolve
POST  /api/admin/reviews/{reviewId}/hide
GET   /api/admin/whatsapp
POST  /api/admin/whatsapp/reconnect

Example Request

# Login, simpan session cookie
curl -i -c cookies.txt \
  -X POST http://localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"password123"}'

# Pakai cookie untuk endpoint bergerbang
curl -i -b cookies.txt http://localhost:8080/api/me

Error Format

Semua error memakai application/problem+json (RFC 9457) dengan 34 error code stabil yang bisa di-switch klien.

{
  "type": "about:blank",
  "title": "Kapasitas tidak mencukupi",
  "status": 409,
  "code": "INSUFFICIENT_CAPACITY",
  "detail": "Kapasitas subkontraktor tidak cukup sampai tenggat yang diminta."
}

Dokumentasi Lengkap

πŸ“– Kontrak OpenAPI 3.1 di contracts/openapi.yaml, peta endpoint terhadap requirement di contracts/README.md. Salinan yang di-embed disinkronkan lewat backend/apidocs-sync.sh, dan CI menggagalkan build bila salinannya basi.

API Eksternal

Satu API pihak ketiga: wilayah.id, sumber wilayah administratif Indonesia. Tanpa API key. Seluruh pemanggilannya di backend/internal/masterdata/regions.go, satu-satunya http.Client keluar di backend.

GET https://wilayah.id/api/provinces.json          β†’ 38 provinsi
GET https://wilayah.id/api/regencies/{kode}.json   β†’ kabupaten/kota per provinsi
Hal Keputusan
Kapan dipanggil Hanya seed:regions --refresh, timeout 30 detik, satu error membatalkan semuanya
Saat melayani pengguna Tidak pernah, wilayah dibaca dari tabel province dan city
Sumber bawaan docs/master-data/regions.json, 38 provinsi dan 514 kabupaten/kota
Idempotensi Upsert pada kode wilayah, nama diperbarui, baris tidak pernah dihapus

Bila wilayah.id mati, aplikasi tetap jalan penuh. seed:regions tanpa --refresh membaca salinan JSON di repository.

Cloudflare, Mailjet, Sentry, dan WhatsApp bukan API data, dicatat di layanan-luar.md.


πŸ§ͺ Testing

Dokumentasi & Hasil Pengujian

Laporan hasil pengujian, dokumen skenario uji manual, dan dokumentasi saat testing tersedia di Google Drive:

Google Drive - Hasil & Dokumentasi Pengujian Devotion

Data Dummy untuk Pengujian

Devotion, data dummy produksi, disimpan di luar repository supaya dump besar tidak ikut ke riwayat kode. Isinya 60 usaha konveksi, 47 listing, dan 34 pesanan di tujuh status, plus antrean admin yang tidak kosong. Semua fiktif, tidak ada data pribadi orang sungguhan.

File Isi
dummy-data.sql seluruh data, satu transaction
creedentials.txt 61 akun uji beserta passwordnya
copy-files.sh penyalin 122 file upload tiruan, Linux dan macOS
copy-files.ps1 penyalin yang sama untuk Windows

Prasyarat impor: migrasi sudah jalan lewat serve, lalu seed:regions dan seed:master-data.

docker compose exec -T postgres psql -U devotion -d devotion < dummy-data.sql

Output terakhir harus COMMIT. Satu error menggagalkan seluruh transaction, jadi tidak ada risiko data separuh jadi. Jalankan salah satu script penyalin agar 122 baris uploaded_file punya file fisiknya di UPLOAD_PATH.

Hanya untuk development dan demo. Jangan diimpor ke database yang memuat data sungguhan.

Running Tests

# Unit tests, backend
cd backend
go vet ./...
go test ./... -p 1

# Integration tests, backend (menjangkau PostgreSQL)
DATABASE_URL_TEST=postgres://devotion:***@127.0.0.1:5434/devotion?sslmode=disable \
  go test ./... -p 1

# Test coverage, backend
go test ./... -p 1 -coverprofile=cover.out
go tool cover -func=cover.out | tail -1

# Unit tests, frontend
cd ../frontend
npm run test -- --testTimeout=30000

# Test coverage, frontend
npm run test:coverage -- --testTimeout=30000

# Linting dan type check
npm run lint
npm run build

DATABASE_URL_TEST wajib disebut eksplisit: bawaannya menunjuk port 5432 sedangkan Compose menerbitkan 5434, dan test yang tidak menjangkau database memilih t.Skip daripada gagal. Tanpa variabel itu seluruh test database dilewati diam-diam dan hasilnya tetap hijau.

--testTimeout=30000 wajib untuk suite OTP (VerifyEmail, VerifyPhone). Test itu mensimulasikan pengetikan enam digit lewat userEvent, dan pada mesin yang lambat prosesnya melewati timeout default Jest 5000 ms. Tanpa flag ini dua suite gagal karena timeout, bukan karena logika salah.

Integration test memakai schema terpisah pada service PostgreSQL yang sama, bukan container tambahan. Test berdeadline memakai Clock yang dapat digantikan, sehingga auto-confirm 7 hari diuji tanpa menunggu 7 hari.


Test Coverage

Angka di bawah diverifikasi terhadap tree ini, bukan disalin dari commit historis.

Backend (Go)

Go packages   : 29
Test files    : 75
Test functions: 427
go test ./...  : lulus
go vet ./...   : lulus

Frontend (TypeScript)

Statements   : 76.41%
Branches     : 57.70%
Functions    : 63.67%
Lines        : 77.22%
Test suites  : 26 lulus, 159 test lulus
ESLint       : lulus, tanpa error
tsc + build  : lulus (TypeScript 5.8, 750 modul ditransformasi)

Coverage frontend dijalankan dengan npm run test:coverage -- --runInBand --testTimeout=30000. Pengujian backend dijalankan dengan go test ./... -p 1, dan pengujian integrasi memakai DATABASE_URL_TEST seperti pada perintah di atas.


πŸ“„ Lisensi

Proyek ini dilisensikan di bawah MIT License - lihat file LICENSE untuk detail lebih lanjut.


Made with ❀️ by Indonesia Emas 74 Kg for ITECHNO CUP 2026

About

Devotion. Delegate your overload production. A platform to offload, orchestrate, and scale your production workload.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages