Skip to content

Branch Office Operations Gap Mining

Scope: branch/headquarters office operations from the legacy admin source. Source controllers mix branch profile, service-area policy, membership fee, bank accounts, image uploads, login audit, and caregiver-facing content. Modern migration should split those into DDD feature slices and use Supabase Auth, Storage, RLS, SpiceDB/OPA authorization, and Strapi where content authoring is the real product.

  • BranchOfficeProfile: branch legal/operational profile, contact info, business number, representative names, address, and active/deleted state.
  • BranchOperator: 현대화의 지점 운영자 actor. 레거시 branch_manager는 경계 alias일 뿐 계약·권한·테스트의 canonical role은 branch_operator다.
  • BranchOperatorCredential: office login identifier, password rotation, token/session state, and duplicate-login guard.
  • BranchSettlementAccount: purpose-specific settlement bank-account snapshot for statement remittance, branch deposit, membership fee, or refund, with owner, masked number, verification state, valid period, supersession, and audit.
  • BranchSettlementAccountSnapshot: immutable account facts frozen for one settlement statement so later branch-account edits cannot change completed or in-progress settlement output.
  • BranchMembershipFeePolicy: monthly branch membership fee snapshot, valid period, and revision history.
  • BranchServiceAreaPolicy: available, consultation-only, and unavailable area rules by address depth and effective period.
  • BranchBrandAsset: branch profile image, stamp image, storage object, version, and deletion/supersession audit.
  • BranchLoginAuditEvent: branch operator login IP, device/user-agent class, auth result, and session id.
  • OfficeContentPublication: branch/HQ-authored manager notices, education schedules, event notices, central notices, and caregiver news.
flowchart LR
  HQ[Headquarters administrator]
  BranchOperator[Branch operator]
  Caregiver[Caregiver portal]

  Profile[BranchOfficeProfile]
  Credential[BranchOperatorCredential]
  SettlementAccount[BranchSettlementAccount]
  AccountSnapshot[BranchSettlementAccountSnapshot]
  Membership[BranchMembershipFeePolicy]
  ServiceArea[BranchServiceAreaPolicy]
  BrandAsset[BranchBrandAsset]
  LoginAudit[BranchLoginAuditEvent]
  Content[OfficeContentPublication]
  Storage[(Supabase Storage)]
  CMS[Strapi CMS]

  HQ --> Profile
  HQ --> Credential
  BranchOperator --> Profile
  Profile --> SettlementAccount
  SettlementAccount --> AccountSnapshot
  Profile --> Membership
  Profile --> ServiceArea
  Profile --> BrandAsset
  Credential --> LoginAudit
  BrandAsset --> Storage
  HQ --> Content
  BranchManager --> Content
  CMS --> Content
  Content --> Caregiver

Source behavior:

  • Office detail GET chooses branch id from the URL for headquarters, but only this read path replaces it with the signed-in branch id for branch users. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:374-381.
  • Office submit accepts the caller-supplied URL $index without applying the GET path’s session-branch substitution, then forwards it as the update key. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:399-448.
  • The first JavaScript guard only marks branch name, membership fee, login id, and password inputs readonly. It does not bind the submitted URL index to the authenticated branch. Evidence: source-refs/sanmopia-admin/static/js/office_management/office_detail.js:1-10.
  • Office submit accepts branch name, representative names, business number, login id, membership pay, description, email, address, phone numbers, primary and secondary bank accounts, office status, and optional password. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:399-452.
  • Duplicate login id is checked before saving. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:186-200.
  • Saving branch profile inserts or updates branch rows; new branch creation also creates an auth token. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:202-259.
  • The browser submit flow reloads the current page and then calls browser history navigation after an update. Evidence: source-refs/sanmopia-admin/static/js/office_management/office_detail.js:253-276.

Modern gap:

  • BranchOfficeProfile and BranchOperatorCredential must be separate. Credential changes need password rotation, session invalidation, audit, and auth provider mapping. Profile changes need revisioned facts.
  • Active/deleted office state should become branch lifecycle state, not raw delete flag copied into domain.
  • Duplicate login checks should be backed by Supabase Auth/unique constraints and explicit conflict responses.
  • Profile edits need expected_revision, typed conflict diffs, and a stable command result route. The browser must not decide success navigation by mixing reload and history state.
  • The own-profile GET and PATCH must take no authoritative branch or actor id. The server resolves exactly one active branch_operator membership from the Supabase session, verifies the branch permission in SpiceDB, and fails closed for zero/multiple membership or pending relationship delivery.
  • The existing generic POST /branch-office-profile-updates is not P02 authority while it accepts branchOfficeProfileId from the caller. It must be hardened behind the same server-owned resolver or closed before P02 can pass; a client-supplied id plus manage check does not prove invitation activation or own-branch scope.
  • PATCH /branch/profile owns only display/contact/structured-address facts. Branch/legal identity, lifecycle, credential, membership fee, raw settlement account, and service-area policy remain separate HQ or feature commands. Exact replay returns the prior full projection; stale revision returns typed 409 with latest projection and field diff; invalid or forbidden fields return typed 422 without partial mutation.

Source behavior:

  • Branch profile stores two bank account sets directly on the branch row. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:420-427 and source-refs/sanmopia-admin/application/models/Branch_model.php:223-231.
  • Saving profile writes a monthly membership fee row keyed by branch and current year/month string. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:261-289.

Modern gap:

  • BranchSettlementAccount should store statement-remittance, branch-deposit, membership-fee, and refund purposes, effective period, verification evidence, masked number, and supersession history. Settlement statements should snapshot account values when a statement is requested.
  • BranchMembershipFeePolicy should be a versioned policy with valid period and branch scope. The current month side effect on profile save is not enough.

Source behavior:

  • Source reads service-available, consultation-only, and unavailable areas as separate accept-type lists. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:291-343.
  • Inserting area rows maps address depth by sido, sigungu, and bname. Consultation/unavailable areas are rejected unless they belong to an existing available area. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:345-433.
  • The office detail page exposes separate buttons for level 1, level 2, and level 3 address scope, and JavaScript normalizes Daum address fields before posting the rule. Evidence: source-refs/sanmopia-admin/application/views/office_management/office_detail.php:222-430 and source-refs/sanmopia-admin/static/js/office_management/office_detail.js:167-246.
  • Area deletion directly deletes by branch and service-area id. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:597-608.

Modern gap:

  • BranchServiceAreaPolicy should store rules by address depth, policy kind, effective period, actor, and revision. Deletes should create superseded facts so historical reservation assignment and pricing eligibility remain readable.
  • Address normalization belongs in a shared location policy helper, not in branch UI/controller code.
  • ServiceCoveragePolicy should evaluate the priority between available, consultation-only, and unavailable rules for a normalized address. Matching, branch routing, and pricing should consume the same decision projection.
  • 2026-07-04 backend/contract update: branch service-area policy publication is now exposed as POST /branch-service-area-policy-revisions and policy lookup is exposed as POST /branch-service-area-policy-decisions. Both derive actor identity from Supabase Auth and enforce branch manage/view through SpiceDB. The decision response carries semantic policy kind, matched rule id, rule revision, address depth, and Daum/Kakao/operator evidence so UI code no longer owns coverage precedence or address-depth branching. The decision path now also records BranchServiceAreaDecisionSnapshot evidence with booking id, decision code, matched rule reference, coverage profile key, and service-calendar profile key. Remaining binding work is reservation workflow invocation, reservation_service_address_id precision, and consumer reads for matching, pricing, branch transfer, and conflict audit.

2026-07-05 persistent-worker tightening:

Missing backend policySource evidenceSource behaviorModern target
Effective-dated service-area publicationsource-refs/sanmopia-admin/application/controllers/OfficeManagement.php:172-207, source-refs/sanmopia-admin/application/models/Branch_model.php:337-426, source-refs/sanmopia-admin/static/js/office_management/office_detail.js:194-245, source-refs/sanmopia-admin/application/views/office_management/office_detail.php:226-430UI posts possible / counsel / impossible plus address depth strings from Daum address data; model inserts current rows with no effective date, expected revision, actor reason, import source, or policy snapshot.BranchServiceAreaPolicyVersion must publish effective-dated rules with actor, expected revision, address-provider evidence, rule fingerprint, policy kind, coverage profile key, service-calendar policy profile key, and impact plan.
Address-match decision snapshotsource-refs/sanmopia_web/application/models/Branch_model.php:38-144, source-refs/sanmopia_web/application/controllers/api/Service.php:47-83, source-refs/sanmopia_web/application/models/Reservation_model.php:2239-2301Booking finds a possible branch by address-level fallback, special split-address handling, and unavailable override; consultation can also come from consultation area, second address, birth/service timing, multi-baby, or picked caregiver.ReservationServiceAreaDecision and ReservationServiceAddressCoverageSnapshot must freeze normalized address, matched possible/consultation/unavailable rule, override reason, consultation reason codes, branch owner, service-address row id, and policy revision before reservation workflow continues.
Service-area withdrawal and branch exception auditsource-refs/sanmopia-admin/application/controllers/OfficeManagement.php:326-344, source-refs/sanmopia-admin/application/models/Branch_model.php:591-607, source-refs/sanmopia-admin/static/js/office_management/office_detail.js:288-317, source-refs/sanmopia-admin/application/views/office_management/office_detail.php:295-430Deletion hard-removes rows by branch/service-area id; branch-user mutation guards are commented in controller/view for consultation and unavailable lists, while possible-area buttons are admin-gated in view only.BranchServiceAreaWithdrawalCommand must supersede rows instead of deleting, reject stale revisions, enforce branch/HQ capability by command policy, record before/after diffs, and emit invalidation for active quotes, matching, settlement, and conflict audit.
Coverage profile consumer bindingsource-refs/sanmopia_web/application/controllers/api/Service.php:47-103, source-refs/sanmopia_web/application/models/Reservation_model.php:2239-2301Coverage result immediately selects branch id and consultation flag, then later readers can reread current mutable branch rows.BranchCoverageProfileDecision must be read by booking, second service address, extension, continuation, branch transfer, pricing, caregiver matching, refund, and settlement consumers from frozen evidence rather than current BRANCH_SERVICE_AREA_TB.

Source behavior:

  • Profile image and stamp image upload replace the branch row path and delete the previous file. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:435-594.
  • Office content image upload copies files across legacy upload folders and returns CKEditor-style payloads. Evidence: source-refs/sanmopia-admin/application/models/Office_management_model.php:12-151.

Modern gap:

  • BranchBrandAsset should be Storage-backed with object key, checksum, content type, owner branch, supersession, delete marker, and audit.
  • Printable documents should reference a versioned stamp/profile asset captured at render time, not mutable branch row paths.
  • Office content media should use Strapi/Supabase Storage asset references, not copied upload directories.

Source behavior:

  • Branch login audit records branch id, IP address, and coarse user-agent browser/robot/mobile classification. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:610-645.

Modern gap:

  • BranchLoginAuditEvent should record auth provider subject, session id, result, IP, parsed device, user-agent, actor relation, and risk metadata.
  • Login audit is security evidence. It should not be optional side-effect code hidden in the branch model.

Office Content For Caregivers And Branches

Section titled “Office Content For Caregivers And Branches”

Source behavior:

  • Office management owns image upload, manager notices, education schedules, manager request list/removal, central notices, branch-facing notices, event notices, Sanmopia news, and FAQ. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:454-1181 and source-refs/sanmopia-admin/application/models/Office_management_model.php:153-577.
  • Manager notice list includes branch-specific rows plus global rows, sorted by fixed flag and created date. Evidence: source-refs/sanmopia-admin/application/models/Office_management_model.php:153-175.

Modern gap:

  • OfficeContentPublication should be CMS-authored where possible. Branch/HQ target scope, fixed/pinned order, publish window, attachments, read tracking, and media references should be modeled explicitly.
  • CaregiverOfficeRequestThread should own manager request list/view/remove behavior, separate from content publications.
  • FAQ/customer support content should live in the customer engagement content context, not branch profile operations.
  • Backend/contract: BranchOfficeProfile command/read model with revision, duplicate-login conflict, branch lifecycle state, and audit.
  • Backend/contract: BranchSettlementAccount with masked account snapshot, verification state, and BranchSettlementAccountSnapshot handoff for settlement statements.
  • Backend/Supabase: SupabaseBranchOfficeProfileRepository persists branch profile current state, revisions, purpose-scoped settlement accounts, membership-fee policies, and operator credential rotations in service-role tables.
  • Backend/contract: BranchMembershipFeePolicy with branch scope and valid period; settlement calculation reads frozen fee policy.
  • Backend/contract: BranchServiceAreaPolicy with address-depth rules, effective period, supersession, and lookup API.
  • Backend/contract: BranchBrandAsset Storage lifecycle and render-time asset capture for documents.
  • Backend/security: BranchLoginAuditEvent from auth/session middleware.
  • CMS/backend: OfficeContentPublication via Strapi with branch/HQ target scope and caregiver portal inbox projection.
  • Backend: CaregiverOfficeRequestThread separate from office content CRUD.
  • Branch profile command split by fact group. Status: Partial. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:399-452, source-refs/sanmopia-admin/application/models/Branch_model.php:160-166, and source-refs/sanmopia-admin/application/models/Branch_model.php:202-259. Source behavior: one office submit saves branch profile, login id/password, membership pay, email/address, phones, two bank account sets, and office status. Backend foundation now exists in backend-repo/src/sanmopia_modernization/domain/branch_operations/features/office_profile/ and backend-repo/src/sanmopia_modernization/application/branch_operations/features/office_profile/. It splits profile, lifecycle, settlement-account, membership-fee, and credential-rotation changes into strict commands with caller-supplied ids, expected revision, actor, reason, and effective timestamps. 2026-07-04 update: settlement account purpose names now use the same contract vocabulary as branch operations (statement_remittance, branch_deposit, membership_fee, and refund), and backend/application contracts can freeze a verified account revision into BranchSettlementAccountSnapshotReadModel for one settlement statement. Snapshot requests reject stale account revisions and unverified accounts, so statement generation does not read mutable branch account rows after the fact. 2026-07-04 runtime update: SupabaseBranchOfficeProfileRepository now persists the split read model through sanmopia_branch_office_profiles, sanmopia_branch_office_profile_revisions, sanmopia_branch_settlement_accounts, sanmopia_branch_membership_fee_policies, and sanmopia_branch_operator_credential_rotations; stage migration, lint, and generated Supabase types were refreshed from the self-hosted stage database. The existing sanmopia_settlement_remittance_account_snapshots table also gained nullable profile/account lineage fields for idempotent frozen statement-account evidence. Follow-up backend update adds BranchSettlementAccountSnapshotService, SupabaseBranchSettlementAccountSnapshotRepository, and POST /branch-settlement-account-statement-snapshots; the API derives actor_id from Supabase Auth, checks branch settle permission through SpiceDB, and persists replay-safe snapshots before settlement documents read account facts. Contract follow-up now adds the public OpenAPI path, JSON schemas, and TS helpers branchSettlementAccountStatementSnapshotsPath() plus branchSettlementAccountStatementSnapshotRequestHeaders(). Profile-edit follow-up added POST /branch-office-profile-updates; it derives the actor from Supabase Auth and checks caller-selected branch manage through SpiceDB, but still accepts authoritative branchOfficeProfileId from the request. It is therefore a generic foundation, not the P02 self-scoped authority, and must be hardened or closed before actor acceptance. Contract follow-up adds OpenAPI, JSON schemas, and TS helpers branchOfficeProfileUpdatesPath() plus branchOfficeProfileUpdateRequestHeaders(). Lifecycle follow-up now adds POST /branch-office-lifecycle-changes; it uses Supabase Auth actor derivation, branch manage, expected revision, and the ubiquitous states onboarding, active, suspended, and closed instead of raw source delete flags. Contract follow-up adds OpenAPI, JSON schema, and TS helpers branchOfficeLifecycleChangesPath() plus branchOfficeLifecycleChangeRequestHeaders(). Settlement-account replacement follow-up now adds POST /branch-settlement-account-replacements; it accepts purpose-scoped, masked-only account facts, derives the actor from Supabase Auth, checks branch manage, and commits a settlement_account revision with expected-revision conflict handling. Contract follow-up adds OpenAPI, JSON schema, and TS helpers branchSettlementAccountReplacementsPath() plus branchSettlementAccountReplacementRequestHeaders(). Remaining gaps: broader self-scoped invitation-activated actor resolution, atomic CAS/idempotency, field-level conflict diffs, retained-audit privacy/reaper policy, UI binding, and service-coverage runtime binding are not accepted yet.

  • Service-area rule supersession. Status: Partial. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:291-343, source-refs/sanmopia-admin/application/models/Branch_model.php:345-433, source-refs/sanmopia-admin/application/views/office_management/office_detail.php:222-430, source-refs/sanmopia-admin/static/js/office_management/office_detail.js:167-246, and source-refs/sanmopia-admin/application/models/Branch_model.php:597-608. Source behavior: AREA_ACCEPT_TYPE_ID_PK values 1, 2, and 3 represent possible, consultation-only, and impossible service areas, and consultation/impossible rows must fit under available-area hierarchy. The browser chooses address depth and prepares address-level fields before posting. Backend/contract update: BranchServiceAreaPolicy now has publish and decision HTTP APIs backed by the existing Supabase repository, semantic policy kinds, effective periods, revision metadata, address-provider evidence, and matched-rule decision output. Remaining gap: bind the decision to booking/extension snapshots, branch matching, pricing, branch-transfer overrides, and conflict audit so later branch rule edits do not rewrite reservation history.

  • Branch profile stale-save and navigation safety. Status: Partial. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:399-452, source-refs/sanmopia-admin/static/js/office_management/office_detail.js:253-276, and source-refs/sanmopia-admin/application/views/office_management/office_detail.php:198-217. Source behavior: one form submit can overwrite branch profile, lifecycle state, membership fee, account fields, and credential fields without a revision predicate, then mixes page reload and browser history navigation. Backend foundation now rejects stale expected_revision values and returns a public branch-office read model from backend-repo/src/sanmopia_modernization/application/branch_operations/features/office_profile/. Remaining gaps: durable conflict records, field-level diff response, persisted audit store, API error mapping, route/read-model response contract, and UI stale-page recovery.

  • Branch asset render snapshot. Status: Pending. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:430-594 and source-refs/sanmopia-admin/application/models/Office_management_model.php:12-151. Source behavior: profile image, stamp image, and content uploads replace mutable file paths and delete previous files. Modern gap: BranchBrandAsset and OfficeContentMediaAsset should preserve previous Storage object, checksum, uploader, supersession reason, and render-time asset snapshot for documents that include branch profile/stamp images.

  • Login audit report alignment. Status: Pending. Evidence: source-refs/sanmopia-admin/application/models/Branch_model.php:610-645 and source-refs/sanmopia-admin/application/models/Statistics_model.php:1971-2137. Source behavior: branch login log is both security evidence and report source for branch connect analytics. Modern gap: BranchLoginAuditEvent should feed AdminAnalyticsCatalog through a read-model projection so security audit fields and aggregated report columns stay consistent.

  • Office-authored content and request routing. Status: Pending. Evidence: source-refs/sanmopia-admin/application/controllers/OfficeManagement.php:454-1181, source-refs/sanmopia-admin/application/models/Office_management_model.php:153-577, source-refs/sanmopia-manager/application/controllers/User.php:398-439, and source-refs/sanmopia-manager/application/models/User_model.php:308-321. Source behavior: manager notices, education schedules, HQ notices, branch notices, event notices, Sanmopia news, FAQ, manager requests, and manager-to-HQ request alerting sit behind office management. Modern gap: OfficeContentPublication needs target audience, branch scope, fixed/pinned order, publish window, media ids, and read projection; CaregiverOfficeRequest needs request state, reply, notification delivery, and support audit.