Skip to content

feat(partner): introduce PARTNER role and /partner endpoint family#3787

Draft
TaprootFreak wants to merge 1 commit into
developfrom
feat/partner-role-and-endpoints
Draft

feat(partner): introduce PARTNER role and /partner endpoint family#3787
TaprootFreak wants to merge 1 commit into
developfrom
feat/partner-role-and-endpoints

Conversation

@TaprootFreak
Copy link
Copy Markdown
Collaborator

Summary

  • New UserRole.PARTNER, wired into RoleGuard fallback table so ADMIN/SUPER_ADMIN inherit access
  • New /partner controller (admin-only, @ApiExcludeController) with 5 endpoints for partner self-service
  • Server-side scope check: caller can only act on users whose usedRef is Config.defaultRef or equals caller's own ref
  • Reuses existing primitives (FeeService.addFeeInternal, UserDataService.removeFee/updateUserDataInternal, UserService.updateUserAdmin); adds two small helpers: UserService.getUsersByUsedRef and FeeService.getCustomFeesForPartner

Why

Referral partners like Fab today rely on the internal DFX Admin Google Sheet (operated by the Compliance team) to set custom onboarding fees on their referees. This PR moves that workflow behind a proper backend, so partners can self-service it from app.dfx.swiss (UI lands in DFXswiss/services, separate PR).

Endpoints

Method Path Purpose
GET /partner/user?address=<addr> Lookup UserData by signing address, scope-filtered
GET /partner/users List own referees (usedRef == caller.ref)
GET /partner/fees List fees the partner is allowed to assign (from customSignUpFees setting)
PUT /partner/user/:userDataId/onboarding addFee + status=Active + usedRef=caller.ref (atomic 3-step)
DELETE /partner/user/:userDataId/fee?fee=<feeId> Remove individual fee

PR Completeness checklist

  • Migration: none required — PARTNER is a new enum value on the existing string-typed role column. History is derived from existing data (no new entity).
  • Environment / Infrastructure: no config or env-var changes.
  • Service updates (frontend): handled in follow-up PRs against DFXswiss/packages and DFXswiss/services.
  • Lint, format, type-check, tests: all green locally (961 passed).

Follow-up PRs

  1. DFXswiss/packages — mirror PARTNER enum value + add PartnerUrl constants in @dfx.swiss/core, publish via OIDC
  2. DFXswiss/servicesusePartnerGuard, usePartner hook, /partner screens (lookup / onboarding form / referee list)

Operational notes

  • After merge to DEV: set Fab's test-account user.role = 'Partner' via DB to test scope behaviour.
  • After merge to PROD: same one-off UPDATE for the actual partner accounts.

Test plan

  • On DEV: login as test partner (role=Partner, ref=158-532)
  • GET /v1/partner/user?address=<known-test-address> returns 200 with canModify=true for in-scope user, 403 for out-of-scope
  • GET /v1/partner/fees returns the configured custom sign-up fees for that partner
  • PUT /v1/partner/user/<id>/onboarding with {feeId} — verify userData.individualFeeList contains the new id, status=Active, all defaultRef-users moved to caller's ref
  • DELETE /v1/partner/user/<id>/fee?fee=<feeId> — verify removal
  • Out-of-scope test: another user (with another usedRef) → 403

Adds a new minimal subdomain enabling referral partners to self-service
their referees on app.dfx.swiss instead of routing through the internal
Google Sheet admin tool.

- New UserRole.PARTNER, wired into RoleGuard fallback table so
  ADMIN/SUPER_ADMIN inherit access
- New /partner endpoint family (admin-only, ApiExcludeController):
    GET    /partner/user?address=<addr>        lookup, scope-filtered
    GET    /partner/users                      list own referees
    GET    /partner/fees                       list partner-allowed fees
    PUT    /partner/user/:id/onboarding        addFee + Active + usedRef
    DELETE /partner/user/:id/fee?fee=<feeId>   remove individual fee
- Server-side scope check: caller can only act on users whose usedRef
  is either Config.defaultRef or equals the caller's own ref
- Reuses existing primitives: FeeService.addFeeInternal,
  UserDataService.removeFee / updateUserDataInternal,
  UserService.updateUserAdmin / getUsersByUsedRef (new),
  FeeService.getCustomFeesForPartner (new wrapper around
  SettingService.getCustomSignUpFees)
- No schema migration: PARTNER is an enum value on the existing
  string-typed role column; history is derived from existing data
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant