다크모드
Leyve 영업 유통 코어 백엔드 계약
목적과 경계
이 코어는 Leyve 자체 상품을 본사와 파트너가 판매할 때 고객계정, 판매귀속, 담당배정과 계정 준비 요청을 일관되게 기록한다.
- CRM은 문의·리드·딜·수주를 소유한다.
- Sales Distribution은 상업적 고객계정과 판매귀속을 소유한다.
- Platform Company/Office/Auth는 실제 테넌트·사용자·권한을 소유한다.
- Partner 정산은 이 코어의 실제 파트너 귀속을 참조하지만 별도 원장이다.
본사를 가상의 파트너로 만들지 않는다. 신규 계약의 seller_category는 first_party | channel_partner이며, 본사와 판매 파트너가 같은 생성 RPC를 사용한다. 수수료·정산 대상은 channel_partner만이다. organization_kind=internal | partner는 배포된 v1 클라이언트 호환 필드로만 유지한다.
스키마
| 테이블 | 책임 |
|---|---|
sales_seller_organizations | 본사·파트너 판매주체와 전역관리/계정생성 능력 |
sales_seller_memberships | 사용자와 판매주체의 역할·활성 멤버십 |
sales_customer_accounts | CRM prospect와 Platform Company 사이의 상업적 고객계정 |
sales_customer_attributions | 최초 유입·판매귀속 fact. append-only |
sales_customer_attribution_semantics | v2 유입경로·등록방식 fact. append-only |
sales_customer_assignments | 현재 영업·서비스·빌링 담당 배정 |
sales_customer_provisioning_requests | Company·관리자·첫 Office·모듈 준비 요청 |
sales_customer_events | append-only 감사 이벤트 |
private.sales_distribution_commands | 멱등 명령의 요청 해시와 응답 저장 |
partner_organizations | Partner Center 계약 상대와 Sales seller 연결 |
partner_memberships | 파트너 사용자·역할·승인 상태 |
partner_applications | 신청·본사 심사·승인 상태 |
private.partner_approval_outbox | 승인과 channel partner 생성 사이의 lease outbox |
sales_company_administrator_invitations | 프로비저닝 관리자 초대·수락 상태 |
private.sales_customer_provisioning_materializations | 요청별 Company·Office 멱등 결과 |
private.sales_product_provisioning_policies | 상품 판매상태·판매주체 종류·Office 에디션 호환 SSOT |
두 작업 큐에는 manual_retry_count, last_manual_retry_at/by/reason이 있다. 자동 시도 횟수는 수동 재시도 때 새 주기로 초기화되지만, 수동 재시도 횟수와 append-only 이벤트로 전체 운영 이력을 보존한다.
RPC 계약
get_my_sales_seller_context_v1
- 인증 사용자의 활성 판매주체 membership을 반환한다.
- 출력은
sellerOrganizationId,sellerCategory,externalPartnerReference, 역할과 계정생성 권한이다. - Partner Center는 임의 seller ID를 입력받지 않고 이 결과와 자신의 파트너 참조가 일치하는 판매주체만 사용한다.
create_sales_customer_account_v2
- 입력: 판매주체, 요청 키, 고객명, 사업자등록번호, 대표/담당 연락처, 실제 유입 경로, 선택적 referral/source request, 등록 방식
- 유입 경로:
phone | website | community | outbound | event | referral | existing_relationship | unknown - 등록 방식:
self_service | first_party_assisted | channel_partner_assisted | admin_import - 유입 경로와 등록 방식은 독립된 fact다. 예: 전화 문의를 파트너가 대신 등록하면
phone + channel_partner_assisted다. - 출력: 생성된 고객계정 ID, 등록번호, 상태,
sellerCategory, 유입 경로와 등록 방식 channel_partner는channel_partner_assisted만 사용할 수 있고,first_party는 이를 사용할 수 없다.- v2는 내부에서 v1 명령의 멱등성·중복검사를 재사용하고, 정규화된 의미를 별도 append-only fact로 저장한다.
create_sales_customer_account_v1 (호환)
- 입력: 판매주체, 요청 키, 고객명, 사업자등록번호, 대표/담당 연락처, 유입 채널, 선택적 referral/source request
registration_method는self_service | headquarters_assisted | partner_assisted를 사용한다.- 출력: 생성된 고객계정 ID, 등록번호, 상태와 판매주체
- 본사 전역관리자는 본사 직판뿐 아니라 실제 파트너를 대신해 그 파트너 귀속 고객도 만들 수 있다.
- 파트너 멤버는 자신의 판매주체만 사용하며
partner_referral | partner_created채널만 허용한다.
request_sales_customer_provisioning_v1
- 입력: 고객계정, 판매주체, 요청 키, 관리자 이메일, 첫 Office 이름/에디션, 요청 모듈 키
- 출력: 프로비저닝 요청 ID와 상태
- 고객계정 상태를
provisioning으로 옮기고 이벤트를 남긴다. - 이 RPC는 Platform Company를 직접 생성하지 않는다. 작업자가 요청을 소비해 Company/Auth/Office API를 각각 멱등 호출해야 한다.
- INSERT trigger가 현재 seller 활성·계정생성 권한, catalog
active, policyavailable, seller category와 Office edition 호환을 검증한다. 프런트 선택지는 권한 근거가 아니다.
list_sales_customer_accounts_v2
- Sales Center와 신규 클라이언트의 기본 조회 RPC다.
- v1 목록 권한·필터를 재사용하고
acquisitionSource,registrationMethod,attributedSellerCategory,provisioningStatus,administratorInvitationStatus를 반환한다. - 아직 v1으로 생성된 고객은 호환 값을 v2 의미로 정규화해 반환한다.
list_sales_customer_accounts_v1 (호환)
- 본사 전역관리자: 전체 또는 지정 판매주체 범위 조회
- 파트너: 자신의 귀속·담당 고객만 조회
- 검색과 lifecycle 필터를 제공한다.
파트너 신청·승인 Bridge
submit_partner_application_v1: 인증 사용자가 파트너 조직·owner membership·신청을 함께 만든다.list_partner_applications_v1: 본사의 승인 권한자가 상태별 신청과 seller 준비 결과를 조회한다.get_my_sales_management_capabilities_v1: 현재 사용자의 파트너 승인·전체 고객 조회 capability를 서버에서 계산한다. UI는 seller 행 속성으로 승인 권한을 추론하지 않는다.approve_partner_application_v1: 본사의 활성first_partyadmin/sales manager만 승인하며 공백이 아닌 1,000자 이하 검토 메모를 서버에서 강제한다. 승인 transaction은partner.approvedoutbox를 함께 기록한다.claim_partner_approval_bridge_v1: service worker가FOR UPDATE SKIP LOCKED로 outbox 하나를 lease한다.bridge_approved_sales_partner_v2: claim한 outbox ID·lease token·worker ID가 현재 유효한 경우에만channel_partnerseller와 최초 admin membership을 멱등 생성한다. v1 bridge는 worker에 공개하지 않는다.complete_partner_approval_bridge_v1/fail_partner_approval_bridge_v1: lease token을 확인해 완료하거나 backoff 후 재시도한다.
bridge 성공 뒤 complete 전에 worker가 종료되어도 안전하다. lease 만료 후 같은 outbox를 다시 claim하고, seller·membership upsert를 재생한 뒤 complete한다. Edge Function은 private outbox를 PostgREST로 직접 읽지 않는다.
Company 프로비저닝 Worker
sales-provisioning-worker Edge Function은 서버 Secret으로만 호출한다. supabase-js Auth Admin과 RPC에는 자동 제공되는 SUPABASE_SERVICE_ROLE_KEY JWT를 사용하며 브라우저에 전달하지 않는다. opaque secret key를 supabase-js bearer token처럼 혼용하지 않는다. 함수는 CORS·OPTIONS를 열지 않는다.
claim_sales_customer_provisioning_v1(worker, leaseSeconds)가 pending/approved, retry 가능 failed, 만료된 provisioning 요청 하나를 claim한다.prepare_sales_customer_provisioning_v1(request, lease)가 현재 lease와 seller·상품 판매상태·Office 호환 policy를 Auth 작업 직전에 재검증한다.resolve_sales_provisioning_auth_user_v1(email)로 기존 Auth 사용자를 확인한다.- 사용자가 없을 때만 Auth Admin
inviteUserByEmail()을 호출한다. resolve와 invite 사이 race가 나면 한 번 더 resolve한다. materialize_sales_customer_provisioning_v1(request, lease, user)가 lease와 현재 policy를 다시 검증한 뒤 한 transaction에서 Company, invited owner membership, Office 0, invited office_admin, Company 계약·item, Office entitlement, invitation과 멱등 결과를 만든다. 이미 결과가 있어도 현재 lease 검증보다 먼저 반환하지 않는다.complete_sales_customer_provisioning_v1이 요청과 고객계정을 완료·active로 옮기고 append-only 완료 이벤트를 기록한다.- 오류는
fail_sales_customer_provisioning_v1이 code/detail/retry_after를 기록한다. 최대 10회 뒤 자동 claim 대상에서 제외한다.
기존·신규 Auth 사용자 모두 이메일 확인만으로 새 Company 권한을 자동으로 받지 않는다. 로그인 사용자는 list_my_pending_sales_company_invitations_v1로 자신의 초대를 확인하고 accept_sales_company_invitation_v1을 명시 호출해야 invited Company·Office membership이 active가 된다. 수락은 다른 사용자의 초대를 처리할 수 없고 멱등이다.
Company 생성과 Auth 초대는 단일 transaction이 될 수 없다. 따라서 Auth 초대는 재사용 가능한 사용자 ID를 만들고, DB materialization은 요청 ID를 고유 키로 사용하는 Saga다. materialization 뒤 complete가 실패해도 같은 요청의 Company·Office를 다시 만들지 않는다.
본사 프로비저닝 운영 RPC
get_my_sales_management_capabilities_v1은canOperateProvisioning을 추가로 반환한다. 활성first_partyseller의admin | sales_manager이고 전 고객 관리 권한이 있어야 참이다.list_sales_provisioning_operations_v1(kind?, limit?)은 미완료 partner outbox와 미완료 customer provisioning request를 한 운영 DTO로 합친다.pending | processing | failed | stuck | exhausted상태, 시도/한도, 다음 처리·lease, 최근 오류, 수동 재시도 감사를 반환한다.retry_sales_provisioning_operation_v1(kind, queueItemId, observedAttempts, reason)은 본사 운영 capability 전용이다. 공백이 아닌 1,000자 이하 사유와 화면이 관찰한 시도 횟수를 요구한다.- mutation은 대상 행을
FOR UPDATE로 잠근 뒤observedAttempts가 현재 attempts와 다르면sales_provisioning_operation_stale, 아직 유효한 lease면sales_provisioning_operation_busy로 거부한다. - customer는 failed 또는 lease가 만료된 provisioning만, partner는 error·10회 소진 또는 lease 만료 상태만 재시도할 수 있다. 완료·정상 대기·정상 처리 중 작업은 되돌리지 않는다.
- 성공하면 자동 attempts를 0으로, 다음 처리 시각을 현재로 되돌리고 lease를 비운다. 기존 오류는 다음 claim이 시작될 때까지 남겨 운영자가 직전 원인을 볼 수 있게 한다.
- customer는
sales.customer_provisioning_manual_retry_requested, partner는partner.approval_bridge_manual_retry_requestedappend-only 이벤트에 actor, 이전 attempts와 reason을 기록한다.
두 RPC는 authenticated에만 execute를 부여하지만 security-definer 내부에서 capability를 다시 계산한다. service_role은 사용자 운영 RPC를 호출하지 않고 기존 worker RPC만 사용한다.
Realtime invalidation
public.sales_customer_events는 supabase_realtime publication에 등록한다. 이벤트 payload는 감사·관찰용이며 UI read model이 아니다. Sales Center와 Partner Center는 허용된 INSERT를 받으면 list_sales_customer_accounts_v2를 재호출한다. RLS는 private.sales_actor_can_read_customer를 그대로 사용한다.
worker 요청의 maxJobs는 파트너와 고객 작업을 합친 총 claim 예산이며 1~20으로 제한한다. 하나라도 fail transition으로 끝나면 HTTP 500과 failedJobs를 반환해 scheduler가 성공으로 오인하지 않게 한다. 인증·환경 오류는 401/503이다.
불변식
first_party판매주체에는 외부 파트너 참조를 두지 않는다.channel_partner는 전 고객 관리 권한을 가질 수 없다.- 최초 판매귀속은 수정·삭제하지 않는다. 운영 담당 변경은 assignment에 남긴다.
- v2에서 판매 파트너는
channel_partner_assisted, 본사는self_service | first_party_assisted | admin_import만 사용할 수 있다. active고객계정에는provisioned_company_id가 반드시 있어야 한다.- 사업자등록번호 중복은 별도 정정·병합 절차 전에는 생성하지 않는다.
- 명령 재시도는 같은 요청 키와 같은 canonical payload면 저장 응답을 재생하고, payload가 다르면 충돌로 거부한다.
- 공개 테이블 직접 쓰기는 금지하고 security-definer RPC만 사용한다.
- worker RPC는
service_role에만 execute를 부여하고 함수 내부에서도 service claim을 확인한다. - Company·Office materialization은 provisioning request당 한 번이며 관리자 수락 전 membership은
invited다. long-term-repair-planning은 서버 catalog 비활성·policyinternal_prototype으로 유지하며 판매 프로비저닝에 포함하지 않는다.- 수동 재시도는 활성 lease를 덮어쓰지 않고, 관찰 attempts가 일치할 때만 새 자동 시도 주기를 연다.
- 수동 재시도 사유·actor·시각은 queue 감사 필드와 append-only 이벤트에 함께 남긴다.
RLS·권한
- 모든
public.sales_*테이블은 RLS가 활성화되어 있다. authenticated에는 필요한 조회 권한만 명시적으로 부여한다.- insert/update/delete는 revoke하고 RPC execute만 부여한다.
- security-definer helper와 private command table은
PUBLIC,anon,authenticated직접 실행·접근을 금지한다. - 파트너 신청 데이터 RLS는
applying | activemembership만 허용하며invited | suspended사용자는 읽을 수 없다. 판매주체 스코프도 JWT 주장만 믿지 않고 활성 membership과 판매주체를 함께 확인한다.
배포·운영 체크리스트
- migration 적용 뒤 SQL smoke와 DB advisor를 실행한다.
sales-provisioning-workerEdge Function에 worker secret과 허용된 초대 redirect를 설정한다.- 운영 스케줄러는
x-worker-secret으로 짧은 간격 POST를 수행한다. 브라우저에서 호출하지 않는다. - Auth 초대 메일 템플릿과 redirect allow-list를 검증한다.
- Sales Center 프로비저닝 운영 카드에서
pending/failed, lease 만료, attempts 10 도달을 확인하고 외부 알림 채널은 후속 연결한다. - 수동 재시도는 오류 원인 조치와 사유 기록 뒤 실행하며, 반복 재시도 횟수도 운영 지표로 감시한다.
- CRM
deal.wonoutbox, 직접가입 webhook, 고객 병합·귀속 보정 워크플로는 후속 범위다.