Skip to content

core: expose vendor approval and suspension via admin API endpoints #98

Description

@EmeditWeb

Problem

Vendors can register but can never be approved, so no vendor can ever receive a loan.

vendor-registry-contract gained a full approval lifecycle in Contracts issue #7: register_vendor() now sets status = VendorStatus::Pending instead of activating immediately, and approve_vendor() and suspend_vendor() were added as admin-only functions. is_active() returns true only for Approved, and creditline-contract's validate_vendor() calls is_active().

Nothing in this API calls any of it. grep -rn "approve_vendor\|approveVendor\|VendorStatus\|suspend_vendor" src/ returns zero matches across the entire repo. VendorRegistryContractClient exposes only isVendorActive() and getVendor(), both read-only. There is no write path to the vendor registry contract at all.

The off-chain side has the same hole. The vendors table created in supabase/migrations/20260504000001_create_vendors_table.sql has no status, approved, or verified column, so there is nowhere to record approval state even locally.

The user-facing outcome: a vendor completes registration through POST /vendors, appears in GET /vendors, shows up in the web app, and is permanently stuck in Pending on-chain. Any learner who tries to take a loan against that vendor is rejected with VendorNotActive. The protocol has no path to onboard a single working vendor.

Ground Rules

This issue must be solved according to StepFi's established engineering standards. Before writing any code:

  1. Read context/architecture-context.md in full
  2. Read context/code-standards.md in full
  3. Read context/progress-tracker.md to understand what has already been built and why, so you do not duplicate or contradict recent work
  4. Read src/modules/vendors/vendors.controller.ts and vendors.service.ts in full to follow the existing controller and service split
  5. Read src/modules/liquidity/liquidity.service.ts for the established pattern of building unsigned XDR and returning it to the client for signing. Clients never call the chain directly
  6. Read src/common/interceptors/audit.interceptor.ts and src/common/decorators/audit-action.decorator.ts, since approval and suspension are privileged actions that must be audit-logged
  7. Read contracts/vendor-registry-contract/src/lib.rs in StepFi-Contracts for the exact approve_vendor and suspend_vendor signatures

This issue depends on the companion issue that introduces AdminGuard and the ADMIN_WALLETS allowlist. There is currently no admin concept in this API. Do not invent a second one here. If that issue has not landed, coordinate rather than duplicating.

Your PR will be rejected regardless of whether CI passes if it conflicts with anything in these files, introduces patterns inconsistent with what already exists, or duplicates functionality that was solved differently elsewhere in the codebase.

What To Build

  1. Add a migration in supabase/migrations/ adding a status column to vendors, constrained to pending, approved, suspended, rejected, defaulting to pending. Timestamp-prefix the filename. Never edit an applied migration. Backfill existing rows to pending so local state matches the on-chain default.
  2. Add write methods to VendorRegistryContractClient: buildApproveVendorXdr(admin, vendor) and buildSuspendVendorXdr(admin, vendor). These build unsigned XDR. They must not submit. Follow the XDR-building pattern in liquidity.service.ts.
  3. Add POST /vendors/:id/approve and POST /vendors/:id/suspend to vendors.controller.ts, both guarded with @UseGuards(JwtAuthGuard, AdminGuard).
  4. Both endpoints return unsigned XDR for the admin wallet to sign, consistent with every other state-changing operation in this API. Do not hold an admin key server-side.
  5. Update the local status column only after the signed transaction is confirmed. Hook this into the existing transaction-status-checker job rather than optimistically writing on XDR build, so a signed-but-never-submitted transaction cannot leave the database claiming a vendor is approved when the chain disagrees.
  6. Reject the transition if the vendor is not currently Pending, matching the contract's VendorNotPending error, and return a 409 with a clear message rather than letting the contract call fail opaquely.
  7. Decorate both endpoints with @AuditAction so approvals and suspensions land in the audit log.
  8. Add status to the vendor DTOs in src/modules/vendors/dto/vendor.dto.ts so clients can distinguish pending from approved vendors.
  9. Full Swagger decorators on both endpoints: @ApiOperation, @ApiResponse for 200, 401, 403, 409, and @ApiTags.
  10. The admin UI for driving these endpoints is out of scope here. That belongs to StepFi-Web core: build full Stellar event indexer service #24 (protocol governance panel). Do not build a UI in this repo.

Files To Touch

  • supabase/migrations/<timestamp>_add_vendor_status.sql (new)
  • src/stellar/contracts/clients/vendor-registry.client.ts
  • src/stellar/contracts/interfaces/vendor-registry.interface.ts
  • src/stellar/contracts/mocks/vendor-registry.mock.ts
  • src/modules/vendors/vendors.controller.ts
  • src/modules/vendors/vendors.service.ts
  • src/modules/vendors/dto/vendor.dto.ts
  • src/jobs/transaction-status-checker/transaction-status-checker.service.ts
  • src/modules/vendors/vendors.service.spec.ts

Acceptance Criteria

  • POST /vendors/:id/approve returns unsigned XDR for an allowlisted admin wallet
  • Both new endpoints return 403 for an authenticated non-admin and 401 when unauthenticated
  • Approving a vendor that is not Pending returns 409, not an opaque contract failure
  • The local status column is written only after on-chain confirmation, never on XDR build
  • A vendor that has been approved on-chain returns true from isVendorActive()
  • Approvals and suspensions appear in the audit log with the acting admin wallet
  • status is present on vendor DTOs and distinguishes pending from approved
  • The migration is a new timestamped file and no applied migration was edited
  • All monetary and state-changing operations validated and safe against invalid input

Mandatory Checks Before Opening PR

  • All context/ files read and understood
  • Code follows context/code-standards.md exactly
  • context/progress-tracker.md updated with an accurate entry describing the change and why
  • Build passes with zero errors (npm run build)
  • All existing tests still pass, test count has not decreased
  • New tests written covering the new functionality
  • No new any types
  • Full Swagger decorators on every new endpoint
  • PR template filled out completely, including the "problem this solves" and "how it was tested" sections
  • PR references this issue number exactly

PRs that fail any check above will be closed without review, regardless of how much work went into them. No exceptions. This is a Grantfox-funded, quality-first project. Code that does not meet these standards will not be merged and will not be paid.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions