English {#english}
prego-saas-app Implementation Plan (Frappe Custom App · SaaS Layer)
Purpose: Specify Frappe app structure, file tree, features, Doctype schema, API, and ops policy for prego-saas-app in a form usable at implementation.
No code generation — plan, design, paths, requirements only.
- Target: Pregoi/prego-saas-app. Business logic only Frappe custom app.
- Assumption: Frappe/ERPNext core unchanged; all extension via hooks, overrides, API, Doctypes inside prego_saas_app.
- Related: prego-docker handles image build, deploy, repository_dispatch; prego-saas-app has no Docker/compose.
1. Two-repo strategy
prego-saas-app: hooks, api, overrides, doctype, public, scheduler (no Docker). prego-docker: Dockerfile, apps.json, compose, GitHub Actions. Benefits: separate versioning (app semver vs image), CI only on app change (repository_dispatch), no Docker tokens in app repo, rollback by commit, dev vs DevOps split, SKU variants via apps.json.
2–onwards. Structure, phases (summary)
- §2 Directory: prego_saas_app/ (hooks, api, overrides, doctype/, config, utils, flags, payroll); setup.py, MANIFEST.in, pyproject.toml; .github/workflows/notify-image-rebuild.yml. Doctypes: prego_tenant, prego_feature_flag, prego_plan_feature, prego_tenant_flag, prego_usage_metric, prego_usage_event, prego_usage_aggregation. No Docker files.
- §3+ Phase 1: minimal validation (test features + workflow). Later phases: provisioning API, billing, Stripe webhook, usage metrics, feature flags, overrides, security. Full phase table and Doctype/API details in Korean section.
Full tables (§1–§2, Phase lists, Doctype specs) and exact wording are in the Korean section below.
한국어 {#korean}
기획서: prego-saas-app 코드 구현 (Frappe Custom App · SaaS 확장 레이어)
목적: prego-saas-app 레포(/Users/marco/prego-saas-app)에 생성할 Frappe 앱 구조·파일 트리·기능 명세·Doctype 스키마·API·운영 정책을 구현 단계에서 그대로 따라 쓸 수 있는 형태로 정리한다.
코드 생성 없음 — 기획·설계·파일 경로·요구사항만 기술.
- 대상 폴더:
/Users/marco/prego-saas-app(GitHub: Pregoi/prego-saas-app). 비즈니스 로직 전용 Frappe Custom App. - 전제: Frappe/ERPNext v15 코어 무수정; 모든 확장은 prego_saas_app 내부에서 hooks·override·API·Doctype으로만 구현.
- 연관: prego-docker-implementation-plan.md — 이미지 빌드·배포·repository_dispatch는 prego-docker 담당. prego-saas-app에는 Docker/compose 없음.
1. 2-Repo 전략과 분리 이유
| 구분 | prego-saas-app | prego-docker |
|---|---|---|
| 역할 | 비즈니스 로직 전용 (Frappe App) | 이미지 빌드·배포 전용 |
| 포함 | hooks, api, overrides, doctype, public, scheduler | Dockerfile, apps.json, compose, GitHub Actions build/promote |
| 제외 | Docker·compose·빌드 스크립트 | 앱 소스 코드 |
| 항목 | 분리 시 장점 |
|---|---|
| 버전 관리 | 앱은 semver, 이미지는 runtime 버전으로 독립 관리 |
| CI 속도 | 앱 변경 시에만 이미지 재빌드(repository_dispatch) |
| 보안 | Docker Hub 토큰을 앱 레포에 두지 않음 |
| 롤백 | 특정 app commit 기반 이미지 복구(commit pin) 가능 |
| 팀 협업 | 개발팀 vs DevOps 분리 |
| SKU 확장 | apps.json 조합만 바꿔 Core/Premium/Enterprise 이미지 생성 가능 |
전체 흐름: prego-saas-app push → repository_dispatch → prego-docker build → Docker Hub push → Staging deploy → 승격 → Production.
2. prego-saas-app 최종 디렉터리 구조 (목표)
구현 단계별로 아래 구조를 채운다. Phase가 올라갈수록 파일·모듈이 추가된다.
prego-saas-app/│├── prego_saas_app/│ ├── __init__.py│ ├── hooks.py│ ├── api.py│ ├── modules.txt│ ├── config/│ │ └── desktop.py│ ├── overrides/ # ERPNext/HRMS 클래스 오버라이드│ │ └── employee.py│ ├── api/ # API 모듈 분리│ │ ├── provisioning.py│ │ ├── billing.py│ │ └── stripe_webhook.py│ ├── utils/│ │ └── security.py│ ├── flags.py # Feature Flag 런타임 조회│ ├── payroll.py # Salary Slip validate 등│ └── doctype/ # Custom Doctypes│ ├── prego_tenant/│ ├── prego_feature_flag/│ ├── prego_plan_feature/│ ├── prego_tenant_flag/│ ├── prego_usage_metric/│ ├── prego_usage_event/│ └── prego_usage_aggregation/│├── setup.py├── MANIFEST.in├── pyproject.toml└── .github/ └── workflows/ └── notify-image-rebuild.yml- Docker 관련 파일·compose 없음.
- notify-image-rebuild.yml: prego-docker 레포의
docs/prego-saas-app-notify-workflow.yml을 복사해 두는 형태. (이미 prego-docker에 템플릿 있음.)
3. Phase 1: 최소 검증용 (테스트용 기능 2종 + 워크플로)
목표: 서버 빌드/배포·custom app 로드·Hook 동작 확인.
3.1 포함 기능
| 기능 | 설명 | 검증 방법 |
|---|---|---|
| Health Check API | GET /api/method/prego_saas_app.api.health_check → {"status":"ok","app":"prego_saas_app","frappe_version":"15.x.x"} | 브라우저/curl 호출 시 JSON 응답 확인. |
| Sales Order Hook | Sales Order after_insert 시 로그 기록 [PREGO TEST] Sales Order Created: <name> | ERPNext에서 SO 생성 후 docker logs <app_container> 확인. |
| (선택) Scheduler | 5분마다 [PREGO TEST] Cron executed 로그 | hooks에 cron 등록, api에 cron_test() 추가. |
3.2 Phase 1 파일 트리
prego-saas-app/├── prego_saas_app/│ ├── __init__.py│ ├── hooks.py # doc_events Sales Order after_insert; (선택) scheduler_events│ ├── api.py # health_check(); log_sales_order(doc, method); (선택) cron_test()│ └── modules.txt # "Prego SaaS App"├── setup.py└── .github/workflows/ └── notify-image-rebuild.yml3.3 테스트 체크리스트
| 항목 | 기대 결과 |
|---|---|
| API 호출 | JSON 응답 정상 |
| Sales Order 생성 | 로그 출력 |
| Scheduler (선택) | 5분마다 로그 |
| 이미지 재빌드 | prego-saas-app push 시 prego-docker 자동 트리거 |
Production 주의: 테스트용 API(health_check)는 staging 전용 브랜치에만 두거나, permission 제한 또는 배포 후 제거 검토.
4. Phase 2: 최소 운영형 Frappe App 템플릿
목표: 앱 메타·데스크탑·모듈·Payroll 확장 예시까지 포함한 “완전한 최소 템플릿”.
4.1 추가 파일
| 경로 | 역할 |
|---|---|
prego_saas_app/config/desktop.py | Workspace/앱 아이콘 등 |
MANIFEST.in, pyproject.toml | 패키징·의존성 |
hooks.py 확장 | app_name, app_title, app_publisher, app_description, app_email, app_license; doc_events (Salary Slip validate); scheduler_events (6시간마다 periodic_task) |
api.py 확장 | periodic_task() |
payroll.py (신규) | Salary Slip validate 시 net pay 0 이하 차단, custom_saas_fee 예시(1%) |
4.2 hooks.py 요구사항 (Phase 2)
- doc_events:
Salary Slip→validate→prego_saas_app.payroll.custom_validate - scheduler_events:
cron→0 */6 * * *→prego_saas_app.api.periodic_task - ERPNext 코드 수정 없이 hooks로만 override.
4.3 payroll.py 요구사항
custom_validate(doc, method):doc.base_net_pay <= 0이면 throw; (예시)doc.base_net_pay * 0.01→doc.custom_saas_fee설정.- Salary Slip에 custom 필드
custom_saas_fee필요 시 Custom Field로 추가(구현 시).
5. Phase 3: SaaS 멀티테넌트 설정 구조
목표: 하나의 이미지·여러 site, site별 DB·정책 분리.
5.1 사이트 구조 개념
sites/├── tenant1.domain.com/│ └── site_config.json├── tenant2.domain.com/│ └── site_config.json└── common_site_config.json- common_site_config.json: db_host, redis_cache, redis_queue, redis_socketio 등 인프라 공통.
- site_config.json (테넌트별): db_name, db_password, prego_plan, max_users 등 SaaS 정책.
5.2 Plan 기반 제한 예시 (Employee)
- hooks:
override_doctype_class→"Employee": "prego_saas_app.overrides.employee.CustomEmployee" - overrides/employee.py:
CustomEmployee(Employee)에서validate()오버라이드;frappe.conf.get("prego_plan")이basic일 때frappe.db.count("Employee") > 5면 throw. - Plan/limit 확장은 이후 Feature Flag·Doctype과 연동.
6. Phase 4: Tenant 프로비저닝 API
목표: Provisioner Site에서만 “신규 테넌트 생성” API 호출 1회로 site 생성 + DB + 앱 설치 + 관리자 생성. 비동기(long queue) 권장.
6.1 권장 구조
- Provisioner 전용 site 1개: 예)
provision.prego.internal. 이 site에서만 테넌트 생성 API 허용. - 파일 위치:
prego_saas_app/api/provisioning.py,prego_saas_app/utils/security.py.
6.2 API 요구사항
- create_tenant(site_name, admin_email, plan)
- POST, whitelist.
_assert_provisioner(): 현재 site가 provisioner site인지, session user가 System Manager인지 검사.- 입력 검증(site_name에
., admin_email에@). frappe.enqueue(..., queue="long")로_create_tenant_job호출.- 반환:
{ "queued": true, "job_id": ..., "site_name": ... }.
6.3 _create_tenant_job 요구사항
- bench new-site, install-app erpnext, hrms, prego_saas_app; set-config prego_plan, prego_admin_email.
- (선택) Prego Tenant Doctype에 레코드 insert(tenant_site, admin_email, plan, status).
- 운영: admin_password는 API 응답에 넣지 말고, 초대 메일/일회성 링크로 전달.
6.4 Prego Tenant Doctype (Phase 4에서 사용할 필드)
- tenant_site, admin_email, plan, status (Trial/Active/Past Due/Suspended/Canceled) 등.
- 전체 스키마는 §10에 통합 정의.
7. Feature Flag 중앙 제어 (Phase 5)
목표: 플래그 우선순위 — (1) 테넌트 override (DB) (2) 플랜 기본값 (DB) (3) 코드 기본값.
7.1 Doctype 3종
| Doctype | 용도 |
|---|---|
| Prego Feature Flag | flag_key(unique), description, default_enabled, category, enterprise_only |
| Prego Plan Feature | plan, flag_key(Link), enabled, limit_value. Composite unique (plan, flag_key). |
| Prego Tenant Flag | tenant_site, flag_key, enabled (테넌트별 override) |
7.2 런타임 조회 (prego_saas_app/flags.py)
- is_enabled(flag_key): (1) Prego Tenant Flag 조회 (2) 없으면 Prego Plan Feature(현재 site의 prego_plan) (3) 없으면 Prego Feature Flag.default_enabled.
- 캐시: Redis 등 short TTL(예: 60초)로 중복 DB 접근 감소.
- 사용 예:
if not is_enabled("payroll_advanced_rules"): frappe.throw(...).
7.3 권한
- Provisioner site에서만 Feature Flag / Plan Feature / Tenant Flag 편집 허용.
- 각 테넌트 site에서는 읽기만 또는 접근 차단.
8. Stripe 연동 (Phase 6)
목표: 테넌트별 Subscription·plan/status 반영, Webhook 수신.
8.1 Prego Tenant 확장 필드 (Stripe)
- stripe_customer_id, stripe_subscription_id, current_period_end, limits_json, flags_json (이미 §10 스키마에 포함).
8.2 Webhook (prego_saas_app/api/stripe_webhook.py)
- webhook(): POST, allow_guest; Provisioner site에서만 수신.
- Stripe 서명 검증 필수(운영).
- 이벤트:
customer.subscription.created/updated→ _handle_subscription;customer.subscription.deleted→ _handle_subscription_deleted. - subscription object에서 customer_id → Prego Tenant 조회 후 plan, status, stripe_subscription_id 업데이트; tenant site에
bench --site <site> set-config prego_plan <plan>반영.
8.3 Checkout Session 생성 (prego_saas_app/api/billing.py)
- create_checkout_session(plan, tenant_name): Stripe Customer 없으면 생성 후 Tenant에 stripe_customer_id 저장; Session.create(mode=subscription, line_items price by plan); success_url/cancel_url 반환.
- price_id ↔ plan 매핑은 Doctype 또는 설정으로 관리 권장.
9. Doctype 스키마 상세 (구현 시 반영)
9.1 Prego Tenant
| Field Label | Fieldname | Type | Options/Notes | Required | Index |
|---|---|---|---|---|---|
| Tenant Site | tenant_site | Data | unique (e.g. tenant1.domain.com) | ✔ | ✔ |
| Domain | domain | Data | FQDN | ✔ | ✔ |
| Plan | plan | Select | basic / pro / enterprise | ✔ | ✔ |
| Status | status | Select | Trial / Active / Past Due / Suspended / Canceled | ✔ | ✔ |
| Admin Email | admin_email | Data | ✔ | ||
| Stripe Customer ID | stripe_customer_id | Data | unique | ✔ | |
| Stripe Subscription ID | stripe_subscription_id | Data | ✔ | ||
| Current Period End | current_period_end | Datetime | |||
| Limits JSON | limits_json | Code | JSON | ||
| Flags JSON | flags_json | Code | JSON | ||
| Disabled | disabled | Check |
Index: tenant_site (unique), stripe_customer_id, status, plan.
9.2 Prego Feature Flag
| Field | Type | Notes |
|---|---|---|
| Flag Key | Data | unique (e.g. payroll_advanced_rules) |
| Description | Small Text | |
| Default Enabled | Check | global default |
| Category | Select | payroll / hr / billing / system |
| Enterprise Only | Check |
Index: flag_key (unique).
9.3 Prego Plan Feature
| Field | Type | Notes |
|---|---|---|
| Plan | Select | basic / pro / enterprise |
| Flag Key | Link | Prego Feature Flag |
| Enabled | Check | |
| Limit Value | Int | optional |
Composite unique: (plan, flag_key).
9.4 Prego Tenant Flag
| Field | Type | Notes |
|---|---|---|
| Tenant Site | Data | |
| Flag Key | Data | |
| Enabled | Check |
테넌트별 플래그 override.
10. Tenant 생성 시 도메인·TLS·Nginx (Phase 4 확장)
- DNS 검증: create_tenant 전/중에 domain이 서버 IP를 가리키는지 확인(socket 등).
- Certbot: provisioning job 내부에서
certbot --nginx -d <domain>(또는 wildcard 시 DNS challenge). - Nginx: tenant별 upstream/ server_name 설정 파일 생성 후
nginx -s reload. - 상세는 Runbook 또는 인프라 레포에서 “Tenant 프로비저닝” 절차로 문서화.
11. Enterprise 이미지 분리 (prego-docker 연동)
- 옵션 A: Enterprise 전용 Frappe App 별도 레포(prego-enterprise-app).
- prego-docker에서 apps-core.json (erpnext + hrms + prego-saas-app), apps-enterprise.json (+ prego-enterprise-app) 분리 빌드.
- 태그: core-, enterprise-.
- 옵션 B: 한 앱 내 enterprise 모듈 + flag 잠금 — 코드가 모든 고객 이미지에 포함되므로 보안/계약 측면에서 A가 유리.
- 기획 단계에서는 옵션 A 권장; 구현 시 prego-docker 쪽 matrix 빌드(§9.2 prego-docker-implementation-plan)와 연동.
12. Provisioner Site 전용 관리자 UI (Phase 7)
목표: 테넌트·플랜·기능 플래그·배포·결제·사용량을 한 흐름으로 운영.
12.1 Workspace
- 이름: “Prego Control Plane”. Provisioner site 전용.
- 메뉴 구조: Tenant (Registry, Create Wizard, Domain & TLS, Health) / Plans & Features (Feature Flags, Plan Features, Tenant Overrides) / Billing (Stripe Customers, Invoices, Usage) / Deployments (Image Catalog, Promote, Rollback) / Operations (Jobs Queue, Audit, Alerts).
12.2 핵심 화면
| 화면 | 내용 |
|---|---|
| Tenant Registry | List: tenant_site, region, plan, status, domain_verified, tls_status, last_deploy_digest, last_seen_at. 액션: View, Suspend/Resume, Change Plan, Rebuild/Deploy, Regenerate Invite, Open Tenant. |
| Create Tenant Wizard | Step: 도메인/리전/플랜/관리자 이메일 → DNS 검증 → Job enqueue → 진행률/완료·초대 링크. |
| Tenant Detail | 탭: Overview, Domain & TLS, Billing, Usage, Deployments. |
| Feature Flag Console | Flag별 기본값, Plan별 enabled/limit, Tenant override 검색/적용. |
| Billing Console | Tenant ↔ Stripe customer/subscription 매핑, 결제 실패 시 Grace → Suspend 정책. |
| Deployment Console | SKU별 image digest 목록, Staging→Production Promote(승인), Blue/Green 스위치/롤백 호출. |
12.3 권한
- Role: Prego Provisioner Admin.
- Prego Tenant, Prego Feature Flag, Prego Plan Feature, Prego Tenant Flag 등: provisioner role만 CRUD.
- 서버/API 레벨: provisioner site가 아니면 provisioning/billing webhook·API 거부.
13. Stripe + Usage 기반 과금 (Phase 8)
목표: 월 구독 + 초과 사용량(metered) Stripe 제출.
13.1 Stripe 측
- Subscription에 metered price line item 추가.
- Usage Record API로 주기적 제출.
13.2 Doctype
| Doctype | 용도 |
|---|---|
| Prego Usage Metric | metric_key(unique), unit, aggregation(sum/max/last), stripe_price_id, enabled |
| Prego Usage Event | (선택) tenant_site, metric_key, quantity, event_ts, idempotency_key, source, raw_json. 트래픽 많으면 집계만 유지. |
| Prego Usage Aggregation | tenant_site, period_start(YYYY-MM-01), metric_key, quantity_sum, quantity_max, last_updated_at, stripe_reported_quantity, stripe_last_reported_at, status(Open/Reported/Error). Unique: (tenant_site, period_start, metric_key). |
13.3 파이프라인
- 앱에서 사용 발생 시 record_usage(metric_key, qty, idempotency_key) 호출 → aggregation만 원자 갱신.
- Scheduler(5~15분)에서 submit_usage_to_stripe(): delta = quantity_sum - stripe_reported_quantity; delta > 0면 Usage Record 생성 후 stripe_reported_quantity 갱신.
- Idempotency: idempotency_key로 중복 무시.
13.4 결제 실패 정책
- past_due → status Past Due; grace period 후 read-only/제한; 지속 미납 → Suspended.
- Webhook으로 정상화 시 Active 복구.
14. Multi-region SaaS (Phase 9, 확장)
목표: 테넌트를 리전별 배치, 데이터 주권·지연 최소화.
14.1 아키텍처
- Control Plane (전역): Provisioner site, Tenant Registry, Billing/Usage, Deployment Orchestrator.
- Regional Data Plane: 리전별 app + mariadb + redis + storage; 테넌트 site 데이터는 해당 리전만.
14.2 라우팅
- 옵션 1 (권장): 리전별 서브도메인 —
tenant.sg.prego.com,tenant.eu.prego.com. - 옵션 2: 동일 도메인 + Geo/DNS 라우팅(고급).
- Prego Tenant.region 필드로 고정; 리전 이동은 마이그레이션 Runbook으로 별도.
14.3 배포
- 이미지 빌드는 전역 1회(digest 고정).
- Promote는 리전별(SG-staging→SG-prod, EU-staging→EU-prod); 리전별 승인 단계 분리 가능.
15. 구현 순서 요약 (Phase)
| Phase | 내용 |
|---|---|
| 1 | 최소 검증: health_check API, Sales Order hook, (선택) scheduler, notify-image-rebuild.yml |
| 2 | 최소 운영 템플릿: desktop, MANIFEST.in, pyproject.toml, payroll custom_validate, periodic_task |
| 3 | SaaS 멀티테넌트: common_site_config/site_config 개념, Employee override(plan별 limit) |
| 4 | Tenant 프로비저닝: create_tenant API, _create_tenant_job, Prego Tenant Doctype, (선택) DNS/Certbot/Nginx |
| 5 | Feature Flag: Prego Feature Flag, Prego Plan Feature, Prego Tenant Flag, flags.py is_enabled |
| 6 | Stripe: Prego Tenant Stripe 필드, webhook, create_checkout_session |
| 7 | Provisioner UI: Workspace, Tenant Registry, Create Wizard, Feature Console, Billing Console, Deployment Console |
| 8 | Usage 과금: Prego Usage Metric/Event/Aggregation, record_usage, submit_usage_to_stripe, idempotency |
| 9 | Multi-region: Control Plane vs Regional Data Plane, region 필드, 리전별 배포 정책 |
16. 초기 스캐폴딩 (bench 명령)
- Frappe v15 bench 환경에서:
bench new-app prego_saas_app(App Title: Prego SaaS App, Publisher: Pregoi, License: MIT 등). - 생성 후 해당 앱 디렉터리를 prego-saas-app Git 레포로 둔 뒤, origin을 Pregoi/prego-saas-app으로 설정.
- 이후 구현은 위 Phase 순서로 파일 추가·수정.
17. 연관 문서
| 문서 | 연계 |
|---|---|
| prego-docker-implementation-plan.md | 이미지 빌드·repository_dispatch·commit pin·compose. |
| provision-tenant-workflow-design.md | 프로비저닝 워크플로·입출력·콜백. |
| docker-hub-stack-ansible-deploy-plan.md | Ansible에서 앱 이미지 소스. |
| naming-conventions.md | 테넌트·site·도메인 네이밍. |
다음 단계: 이 기획서를 바탕으로 /Users/marco/prego-saas-app(Pregoi/prego-saas-app)에 Phase 1부터 순서대로 코드·Doctype·워크플로를 구현한다.