Files
apskel-owner-flutter/docs/migrasi-profit-sharing.md
T
efrilm 243b7e02aa
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s
feat: update profit shareing
2026-10-04 22:31:01 +07:00

13 KiB
Raw Blame History

Migrasi profit sharing

4 Oktober 2026

Ringkasan

Ada tiga perubahan di backend:

  1. Parent category bisa ditandai bukan team. Kategori punya field baru is_team. Parent category dengan is_team: false tidak bisa dipilih sebagai team di purchase order dan cash advance, dan tidak ikut laporan profit sharing.
  2. Endpoint laporan pindah path. /api/v1/analytics/parent-categories menjadi /api/v1/analytics/profit-sharing.
  3. Pembagian revenue tinggal dua porsi. Porsi purchase (60%) dihapus. Revenue sekarang dibagi ke owner (SDL) dan team, dengan porsi team = 100% − fee owner.

Nomor 2 dan 3 adalah breaking change. Setelah backend baru dirilis, client yang masih memanggil path lama mendapat 404, dan limit_purchase serta percentages.purchase tidak ada lagi di response. Karena itu client harus diupdate lebih dulu, lihat Urutan rilis.

Yang perlu bertindak, di dashboard maupun app mobile, mana pun yang punya layarnya:

  • Form kategori: tambah toggle team untuk parent category.
  • Laporan profit sharing: ganti path, hapus porsi purchase, tampilkan dua porsi.
  • Form purchase order dan cash advance: picker team tidak perlu diubah, tapi form edit perlu menyesuaikan, lihat Purchase order dan cash advance.

Kategori: field is_team

Response

Semua response kategori membawa is_team (boolean, tidak pernah null): POST /api/v1/categories, PUT /api/v1/categories/:id, GET /api/v1/categories, dan GET /api/v1/categories/:id.

{
  "id": "<uuid>",
  "name": "Merchandise",
  "parent_id": null,
  "owner_fee_percent": null,
  "is_team": false
}

Request

POST /api/v1/categories dan PUT /api/v1/categories/:id menerima is_team.

{
  "name": "Merchandise",
  "is_team": false
}
Request is_team yang dikirim Hasil
Create tidak dikirim true
Create false false
Update tidak dikirim atau null tidak berubah
Update true atau false diganti

Update yang hanya berisi is_team diterima.

Aturan nilainya:

  • is_team hanya berpengaruh di parent category, yaitu kategori dengan parent_id: null. Di sub-category nilainya disimpan tapi tidak dipakai, jadi tampilkan toggle hanya untuk parent category.
  • Semua kategori yang sudah ada sebelum rilis bernilai true. Tidak ada yang berubah sampai admin mematikannya.
  • Flag dibaca saat request, bukan saat transaksi. Kalau parent category dimatikan, penjualannya di periode lampau juga hilang dari laporan profit sharing. Kalau dinyalakan lagi, semuanya muncul kembali.

Efek is_team: false

Endpoint Efek
GET /api/v1/purchase-orders/teams, GET /api/v1/cash-advances/teams Kategori tidak muncul. Pusat tetap ada.
Create dan update purchase order dan cash advance team_scope: "category" dengan team_category_id kategori ini ditolak.
GET /api/v1/analytics/profit-sharing Kategori tidak muncul di data, dan revenue-nya tidak dihitung di budget.
GET /api/v1/analytics/profit-sharing/:parent_category_id Ditolak.

Data yang sudah ada tidak diubah. Purchase order dan cash advance yang sudah tercatat ke kategori itu tetap menyimpan team-nya. Laporan purchasing (GET /api/v1/analytics/purchasing) masih menampilkannya di team_data, dan filter team=<category_id> tetap bisa dipakai.

Laporan lain yang tidak menyaring is_team, misalnya GET /api/v1/analytics/categories, tetap menampilkan penjualan kategori itu.

Purchase order dan cash advance

Picker team sudah mengambil dari GET /api/v1/purchase-orders/teams dan GET /api/v1/cash-advances/teams, jadi kategori non-team otomatis tidak muncul tanpa perubahan di client.

Yang perlu diubah ada di form edit. Purchase order atau cash advance lama bisa tercatat ke kategori yang sekarang sudah non-team. Kalau form edit mengirim ulang team_scope dan team_category_id yang sama, request ditolak, walaupun user tidak mengubah team-nya.

  1. Kirim team_scope dan team_category_id hanya kalau user mengganti team. Kalau team_scope tidak dikirim, team yang tersimpan tidak berubah.
  2. Kalau team yang tersimpan tidak ada di daftar /teams, tampilkan namanya dari field team di response apa adanya, dan jangan memilihkan team lain secara otomatis.

Contoh error saat team ditolak, dengan HTTP status 500:

{
  "success": false,
  "data": null,
  "errors": [
    {
      "code": "900",
      "entity": "purchase_order_service",
      "cause": "category Merchandise is not a team"
    }
  ]
}

Untuk cash advance, entity bernilai cash_advance_service. Cocokkan dengan teks is not a team di cause kalau perlu menampilkan pesan khusus.

Laporan profit sharing

Path baru

Lama Baru
GET /api/v1/analytics/parent-categories GET /api/v1/analytics/profit-sharing
GET /api/v1/analytics/parent-categories/:parent_category_id GET /api/v1/analytics/profit-sharing/:parent_category_id

Query parameter dan role tidak berubah:

  • date_from dan date_to wajib, dengan format DD-MM-YYYY, misalnya 28-09-2026.
  • outlet_id opsional.
  • Hanya bisa diakses superadmin, admin, manager, owner, dan purchasing.

Path lama sudah tidak ada dan mengembalikan 404.

Pembagian revenue

Revenue tiap parent category yang team dibagi dua:

  • SDL (fee owner) = revenue × owner_fee_percent / 100. Default-nya 20%, dan bisa diganti per parent category lewat owner_fee_percent di kategori.
  • Team = revenue − SDL.

Contoh dengan tiga parent category dalam satu minggu:

Parent category is_team Fee owner Revenue SDL Team
Food true 20% (default) 1.000.000 200.000 800.000
Drink true 35% 2.000.000 700.000 1.300.000
Merchandise false - 500.000 tidak dihitung tidak dihitung
Budget minggu ini 3.000.000 900.000 2.100.000

Perubahan field

Field Sebelum Sesudah
data[] semua parent category hanya parent category team
budget.percentages.purchase 60 dihapus
budget.percentages.owner 20, atau fee parent itu di endpoint detail tidak berubah
budget.percentages.team 20 100 − owner: 80 di list, 100 − fee parent di detail
limit_purchase di budget.total, budget.weekly[], budget.monthly[] 60% revenue dihapus
limit_team di tempat yang sama 20% revenue revenue − sdl
revenue dan sdl di budget semua parent category hanya parent category team

Di endpoint list, budget.percentages selalu berisi default 20 dan 80, walaupun ada parent dengan fee berbeda. Angka sdl dan limit_team dihitung per parent dengan fee masing-masing, jadi limit_team / revenue bisa tidak persis 80%. Tampilkan angka rupiah dari response, jangan dihitung ulang dari persentase.

Baris di data[] membawa sdl tapi tidak membawa porsi team. Kalau porsi team per parent perlu ditampilkan, hitung dari total_revenue − sdl.

Contoh response list, dipotong:

{
  "success": true,
  "data": {
    "date_from": "2026-09-28T00:00:00+07:00",
    "date_to": "2026-10-04T23:59:59.999999999+07:00",
    "data": [
      {
        "parent_category_id": "<uuid>",
        "parent_category_name": "Drink",
        "owner_fee_percent": 35,
        "sdl": 700000,
        "total_revenue": 2000000
      },
      {
        "parent_category_id": "<uuid>",
        "parent_category_name": "Food",
        "owner_fee_percent": 20,
        "sdl": 200000,
        "total_revenue": 1000000
      }
    ],
    "budget": {
      "percentages": { "owner": 20, "team": 80 },
      "cut_off_from": "2026-09-28T00:00:00+07:00",
      "cut_off_to": "2026-10-04T23:59:59.999999999+07:00",
      "total": {
        "period_start": "2026-09-28T00:00:00+07:00",
        "period_end": "2026-10-04T23:59:59.999999999+07:00",
        "revenue": 3000000,
        "order_count": 4,
        "sdl": 900000,
        "limit_team": 2100000
      },
      "weekly": [
        {
          "period_start": "2026-09-28T00:00:00+07:00",
          "period_end": "2026-10-04T23:59:59.999999999+07:00",
          "revenue": 3000000,
          "order_count": 4,
          "sdl": 900000,
          "limit_team": 2100000
        }
      ],
      "monthly": [
        {
          "month": "2026-09",
          "week_count": 1,
          "period_start": "2026-09-28T00:00:00+07:00",
          "period_end": "2026-10-04T23:59:59.999999999+07:00",
          "revenue": 3000000,
          "order_count": 4,
          "sdl": 900000,
          "limit_team": 2100000
        }
      ]
    }
  },
  "errors": null
}

Di endpoint detail, budget bentuknya sama, tapi percentages memakai fee parent itu, misalnya { "owner": 35, "team": 65 } untuk Drink.

Detail kategori non-team

Detail untuk parent category non-team ditolak dengan HTTP status 500:

{
  "success": false,
  "data": null,
  "errors": [
    {
      "code": "internal_error",
      "entity": "AnalyticsHandler::GetParentCategoryAnalyticsDetail",
      "cause": "failed to get parent category analytics detail: failed to get parent category analytics detail: category Merchandise is not a team"
    }
  ]
}

Ini bisa terjadi kalau user membuka link lama atau bookmark ke parent yang baru dimatikan. Cocokkan dengan teks is not a team di cause, lalu arahkan user kembali ke list.

Migrasi client

Laporan profit sharing

  1. Ganti path ke /api/v1/analytics/profit-sharing. Selama backend lama masih jalan, path baru mengembalikan 404. Kalau dapat 404, panggil path lama /api/v1/analytics/parent-categories, supaya client baru bisa dirilis sebelum backend.
  2. Hapus tampilan limit purchase dan persentase purchase. Jangan menganggap limit_purchase atau percentages.purchase selalu ada.
  3. Tampilkan dua porsi dengan label "SDL / Fee owner" dan "Team", ambil angkanya dari sdl dan limit_team.
  4. Kalau detail ditolak dengan is not a team, arahkan kembali ke list.

Selama fallback ke backend lama, limit_team masih berisi 20% revenue. Angkanya baru jadi revenue − sdl setelah backend baru dirilis.

Form kategori

  1. Tambah toggle is_team untuk parent category, misalnya berlabel "Ikut profit sharing (team)". Untuk kategori baru, toggle menyala secara default.
  2. Saat membuka form edit, isi toggle dari is_team. Selama backend lama masih jalan, field ini tidak ada di response, jadi anggap true.
  3. Saat admin mematikan toggle, tampilkan konfirmasi bahwa kategori itu tidak akan muncul di pilihan team dan di laporan profit sharing, termasuk untuk periode lampau.
  4. Di daftar kategori, beri penanda untuk parent category dengan is_team: false.

Backend lama mengabaikan is_team di request, jadi toggle bisa dirilis lebih dulu, tapi belum berpengaruh sampai backend baru jalan. Pengecualiannya update yang hanya berisi is_team: backend lama menolaknya dengan error code 303 dan pesan at least one field must be provided for update. Selama masa transisi, kirim is_team bersama field form lainnya.

Database dan urutan rilis

Migration 000101_add_is_team_to_categories menambah kolom categories.is_team (BOOLEAN NOT NULL DEFAULT TRUE). Semua kategori yang ada otomatis bernilai true. Migration ini hanya menambah kolom, jadi backend lama tetap jalan normal setelahnya.

Urutan rilis:

  1. Rilis client baru: path profit sharing dengan fallback ke path lama, tanpa porsi purchase, dan dengan toggle is_team. Untuk app mobile, pastikan versi baru sudah dipakai sebagian besar user sebelum langkah 3, karena versi lama yang memanggil /parent-categories mendapat 404 setelah itu.
  2. Jalankan migration 000101.
  3. Deploy backend baru.
  4. Hapus fallback path lama di client.
  5. Admin mematikan is_team di parent category yang bukan team.

Rollback: deploy backend lama, lalu jalankan down migration yang menghapus kolom is_team. Client dengan fallback tetap jalan di backend lama. Nilai is_team yang sudah diatur admin hilang saat kolom dihapus.

Checklist

  • Client baru (fallback path, tanpa porsi purchase, toggle is_team) dirilis
  • Migration 000101 dan backend baru dirilis di staging
  • Uji: parent category yang dimatikan hilang dari /purchase-orders/teams dan /cash-advances/teams
  • Uji: purchase order dan cash advance baru dengan kategori itu ditolak, dan edit purchase order lama tanpa mengganti team tetap berhasil
  • Uji: kategori itu hilang dari /analytics/profit-sharing, dan detailnya ditolak
  • Uji: limit_team = revenue − sdl, dan sdl mengikuti fee masing-masing parent
  • Migration 000101 dan backend baru dirilis di production
  • Fallback path lama di client dihapus

FAQ

Kenapa porsi team jadi 100% − fee owner, bukan tetap 20%? Porsi purchase sudah tidak ada, jadi seluruh revenue dibagi dua. Owner mengambil fee-nya, dan sisanya untuk team. Kalau fee owner sebuah parent dinaikkan, porsi team parent itu turun sebesar yang sama.

Apakah sub-category bisa dijadikan non-team sendiri? Tidak. Team dan profit sharing dihitung per parent category, jadi semua sub-category ikut status parent-nya.

Bagaimana dengan kategori top-level yang tidak punya sub-category? Kategori itu tetap parent category, jadi is_team berlaku untuknya.