merge.py + referrals: codegen gate GREEN (discriminator mappings + self-contained) (#30)
Two remaining gate failures fixed: - prefix() now namespaces discriminator.mapping values (ref-STRINGS, not $ref keys) → platform DeployJob*/CancelDeploymentJob* no longer dangle. - referrals was the one spec using cross-file ../shared/errors.yaml refs → inlined the error responses (BadRequest/Unauthorized/Forbidden/NotFound/InternalServerError) + Error schema, rewrote refs local. Self-contained like every other spec. Exact CI gate: dupIds=0 dangling=0 GATE PASS. Unblocks regenerate-sdks.yml.
This commit is contained in:
+107
-15
@@ -1540,9 +1540,9 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_AdminListEnvelope'
|
||||
'403':
|
||||
$ref: ../shared/errors.yaml#/components/responses/Forbidden
|
||||
$ref: '#/components/responses/referrals_Forbidden'
|
||||
'500':
|
||||
$ref: ../shared/errors.yaml#/components/responses/InternalServerError
|
||||
$ref: '#/components/responses/referrals_InternalServerError'
|
||||
/v1/admin/referrals/sweep:
|
||||
post:
|
||||
operationId: referrals_adminSweepReferrals
|
||||
@@ -1566,9 +1566,9 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_AdminSweepEnvelope'
|
||||
'403':
|
||||
$ref: ../shared/errors.yaml#/components/responses/Forbidden
|
||||
$ref: '#/components/responses/referrals_Forbidden'
|
||||
'500':
|
||||
$ref: ../shared/errors.yaml#/components/responses/InternalServerError
|
||||
$ref: '#/components/responses/referrals_InternalServerError'
|
||||
/v1/admin/revenue:
|
||||
get:
|
||||
tags:
|
||||
@@ -52577,9 +52577,9 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_MyReferralsResponse'
|
||||
'403':
|
||||
$ref: ../shared/errors.yaml#/components/responses/Forbidden
|
||||
$ref: '#/components/responses/referrals_Forbidden'
|
||||
'500':
|
||||
$ref: ../shared/errors.yaml#/components/responses/InternalServerError
|
||||
$ref: '#/components/responses/referrals_InternalServerError'
|
||||
/v1/referrals/claim:
|
||||
post:
|
||||
operationId: referrals_claimReferral
|
||||
@@ -52619,13 +52619,13 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_ClaimResponse'
|
||||
'400':
|
||||
$ref: ../shared/errors.yaml#/components/responses/BadRequest
|
||||
$ref: '#/components/responses/referrals_BadRequest'
|
||||
'403':
|
||||
$ref: ../shared/errors.yaml#/components/responses/Forbidden
|
||||
$ref: '#/components/responses/referrals_Forbidden'
|
||||
'404':
|
||||
$ref: ../shared/errors.yaml#/components/responses/NotFound
|
||||
$ref: '#/components/responses/referrals_NotFound'
|
||||
'500':
|
||||
$ref: ../shared/errors.yaml#/components/responses/InternalServerError
|
||||
$ref: '#/components/responses/referrals_InternalServerError'
|
||||
/v1/referrals/health:
|
||||
get:
|
||||
operationId: referrals_referralsHealth
|
||||
@@ -77216,8 +77216,8 @@ components:
|
||||
discriminator:
|
||||
propertyName: applicationType
|
||||
mapping:
|
||||
application: '#/components/schemas/CancelDeploymentJobApplication'
|
||||
compose: '#/components/schemas/CancelDeploymentJobCompose'
|
||||
application: '#/components/schemas/platform_CancelDeploymentJobApplication'
|
||||
compose: '#/components/schemas/platform_CancelDeploymentJobCompose'
|
||||
platform_CancelDeploymentJobApplication:
|
||||
type: object
|
||||
required:
|
||||
@@ -77334,9 +77334,9 @@ components:
|
||||
discriminator:
|
||||
propertyName: applicationType
|
||||
mapping:
|
||||
application: '#/components/schemas/DeployJobApplication'
|
||||
compose: '#/components/schemas/DeployJobCompose'
|
||||
application-preview: '#/components/schemas/DeployJobApplicationPreview'
|
||||
application: '#/components/schemas/platform_DeployJobApplication'
|
||||
compose: '#/components/schemas/platform_DeployJobCompose'
|
||||
application-preview: '#/components/schemas/platform_DeployJobApplicationPreview'
|
||||
platform_DeployJobApplication:
|
||||
type: object
|
||||
required:
|
||||
@@ -79219,6 +79219,32 @@ components:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Unix timestamp.
|
||||
referrals_Error:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- msg
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- error
|
||||
description: Always "error" for error responses
|
||||
msg:
|
||||
type: string
|
||||
description: Human-readable error message
|
||||
code:
|
||||
type: integer
|
||||
description: Application-specific error code
|
||||
data:
|
||||
type: object
|
||||
description: Additional error context
|
||||
data2:
|
||||
type: object
|
||||
description: Additional error details
|
||||
requestId:
|
||||
type: string
|
||||
description: Request ID for debugging
|
||||
referrals_MyReferralView:
|
||||
type: object
|
||||
description: One row in the referrer's own list (their side of the edge).
|
||||
@@ -82714,6 +82740,72 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/provisioning_Error'
|
||||
referrals_BadRequest:
|
||||
description: Bad Request - Invalid input parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_Error'
|
||||
examples:
|
||||
invalidParameter:
|
||||
value:
|
||||
status: error
|
||||
msg: 'Invalid parameter: email format is incorrect'
|
||||
code: 400
|
||||
referrals_Forbidden:
|
||||
description: Forbidden - Insufficient permissions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_Error'
|
||||
examples:
|
||||
insufficientPermissions:
|
||||
value:
|
||||
status: error
|
||||
msg: You don't have permission to access this resource
|
||||
code: 403
|
||||
referrals_InternalServerError:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_Error'
|
||||
examples:
|
||||
serverError:
|
||||
value:
|
||||
status: error
|
||||
msg: An internal error occurred
|
||||
code: 500
|
||||
requestId: req_abc123
|
||||
referrals_NotFound:
|
||||
description: Not Found - Resource does not exist
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_Error'
|
||||
examples:
|
||||
resourceNotFound:
|
||||
value:
|
||||
status: error
|
||||
msg: Resource not found
|
||||
code: 404
|
||||
referrals_Unauthorized:
|
||||
description: Unauthorized - Authentication required
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/referrals_Error'
|
||||
examples:
|
||||
missingToken:
|
||||
value:
|
||||
status: error
|
||||
msg: Authentication required
|
||||
code: 401
|
||||
invalidToken:
|
||||
value:
|
||||
status: error
|
||||
msg: Invalid or expired token
|
||||
code: 401
|
||||
registry_BadRequest:
|
||||
description: Bad Request
|
||||
content:
|
||||
|
||||
@@ -98,6 +98,14 @@ def prefix(node, svc):
|
||||
name = "/".join(v.split("/")[3:])
|
||||
out[k] = f"#/components/{kind}/{svc}_{name}"
|
||||
continue
|
||||
if k == "mapping" and isinstance(v, dict):
|
||||
# discriminator.mapping values are component-ref STRINGS (not $ref
|
||||
# keys) — namespace them too, or they dangle post-merge.
|
||||
out[k] = {mk: (f"#/components/schemas/{svc}_{mv.split('/', 3)[-1]}"
|
||||
if isinstance(mv, str) and mv.startswith("#/components/schemas/")
|
||||
else mv)
|
||||
for mk, mv in v.items()}
|
||||
continue
|
||||
out[k] = prefix(v, svc)
|
||||
return out
|
||||
if isinstance(node, list):
|
||||
|
||||
+169
-56
@@ -2,27 +2,17 @@ openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Referrals API
|
||||
summary: Per-org viral-loop referral program that grants promo cloud credit (hanzoai/cloud clients/referrals).
|
||||
description: |
|
||||
The Hanzo Cloud referrals surface served by the `referrals` subsystem of the
|
||||
unified cloud binary (HIP-0106), reached through the gateway at `api.hanzo.ai`.
|
||||
|
||||
A native-Go, per-org referral program on Base/SQLite that grants promo cloud
|
||||
credit through the same commerce ledger path as an admin comp (the trial/Credit
|
||||
bucket, tag `grant:referral`).
|
||||
|
||||
The loop, end to end:
|
||||
1. Every org has a STABLE referral code and a link `https://<brand>/?ref=<code>`.
|
||||
2. A new org signs up via a link, the console posts `POST /v1/referrals/claim`
|
||||
with the code, recording referrer to referee at status `signed_up`.
|
||||
Self-referral is blocked; one referral per referee ever (idempotent).
|
||||
3. When the referee QUALIFIES (has made metered spend) both sides are granted
|
||||
trial credit: referrer +$10 (1000 cents), referee +$5 (500 cents). The
|
||||
grant is latched at-most-once. The qualify check runs lazily when the
|
||||
referrer loads `GET /v1/referrals` and via the admin sweep.
|
||||
|
||||
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
|
||||
token authenticates every Hanzo service. The customer routes are scoped to the
|
||||
caller's org server-side; the `/v1/admin/referrals*` routes require global-admin.
|
||||
description: "The Hanzo Cloud referrals surface served by the `referrals` subsystem of the\nunified cloud binary (HIP-0106),\
|
||||
\ reached through the gateway at `api.hanzo.ai`.\n\nA native-Go, per-org referral program on Base/SQLite that grants promo\
|
||||
\ cloud\ncredit through the same commerce ledger path as an admin comp (the trial/Credit\nbucket, tag `grant:referral`).\n\
|
||||
\nThe loop, end to end:\n 1. Every org has a STABLE referral code and a link `https://<brand>/?ref=<code>`.\n 2. A new\
|
||||
\ org signs up via a link, the console posts `POST /v1/referrals/claim`\n with the code, recording referrer to referee\
|
||||
\ at status `signed_up`.\n Self-referral is blocked; one referral per referee ever (idempotent).\n 3. When the referee\
|
||||
\ QUALIFIES (has made metered spend) both sides are granted\n trial credit: referrer +$10 (1000 cents), referee +$5\
|
||||
\ (500 cents). The\n grant is latched at-most-once. The qualify check runs lazily when the\n referrer loads `GET\
|
||||
\ /v1/referrals` and via the admin sweep.\n\nAuthentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The\
|
||||
\ same\ntoken authenticates every Hanzo service. The customer routes are scoped to the\ncaller's org server-side; the\
|
||||
\ `/v1/admin/referrals*` routes require global-admin.\n"
|
||||
version: 8.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
@@ -47,13 +37,18 @@ paths:
|
||||
/v1/referrals:
|
||||
get:
|
||||
operationId: getMyReferrals
|
||||
tags: [Referrals]
|
||||
tags:
|
||||
- Referrals
|
||||
summary: Get my referral code, link, and referrals
|
||||
description: |
|
||||
Returns the caller org's stable referral code and link, the referrals they
|
||||
description: 'Returns the caller org''s stable referral code and link, the referrals they
|
||||
|
||||
have made (with per-referral status and credit earned), a status tally, and
|
||||
|
||||
the total credit earned. Opportunistically runs the qualify check for the
|
||||
caller's own pending referees, so the page is self-updating.
|
||||
|
||||
caller''s own pending referees, so the page is self-updating.
|
||||
|
||||
'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
@@ -64,20 +59,26 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MyReferralsResponse'
|
||||
'403':
|
||||
$ref: '../shared/errors.yaml#/components/responses/Forbidden'
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '../shared/errors.yaml#/components/responses/InternalServerError'
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/v1/referrals/claim:
|
||||
post:
|
||||
operationId: claimReferral
|
||||
tags: [Referrals]
|
||||
tags:
|
||||
- Referrals
|
||||
summary: Claim a referral from a ?ref code
|
||||
description: |
|
||||
Records a referral. The REFEREE is the validated caller (never client-
|
||||
description: 'Records a referral. The REFEREE is the validated caller (never client-
|
||||
|
||||
supplied); the referrer is resolved from the code. Idempotent (one per
|
||||
|
||||
referee, first-touch wins); self-referral is blocked; an unknown code is
|
||||
|
||||
rejected. Returns 201 when a new referral was created, 200 when it already
|
||||
|
||||
existed.
|
||||
|
||||
'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
@@ -100,29 +101,33 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ClaimResponse'
|
||||
'400':
|
||||
$ref: '../shared/errors.yaml#/components/responses/BadRequest'
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '../shared/errors.yaml#/components/responses/Forbidden'
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '../shared/errors.yaml#/components/responses/NotFound'
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '../shared/errors.yaml#/components/responses/InternalServerError'
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/v1/admin/referrals:
|
||||
get:
|
||||
operationId: adminListReferrals
|
||||
tags: [Admin]
|
||||
tags:
|
||||
- Admin
|
||||
summary: List every referral with a fleet summary (global-admin)
|
||||
description: |
|
||||
Returns every referral (both orgs exposed) plus a fleet summary. Global-admin
|
||||
description: 'Returns every referral (both orgs exposed) plus a fleet summary. Global-admin
|
||||
|
||||
only. Wrapped in the `{ status, msg, data }` admin envelope.
|
||||
|
||||
'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Max rows to return. Defaults to 500 when absent/invalid/<=0; capped at 1000.
|
||||
description: 'Max rows to return. Defaults to 500 when absent/invalid/<=0; capped at 1000.
|
||||
|
||||
'
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
@@ -136,18 +141,22 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AdminListEnvelope'
|
||||
'403':
|
||||
$ref: '../shared/errors.yaml#/components/responses/Forbidden'
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '../shared/errors.yaml#/components/responses/InternalServerError'
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/v1/admin/referrals/sweep:
|
||||
post:
|
||||
operationId: adminSweepReferrals
|
||||
tags: [Admin]
|
||||
tags:
|
||||
- Admin
|
||||
summary: Qualify-check every pending referral (global-admin)
|
||||
description: |
|
||||
The periodic qualify path (cron/operator on demand). Qualify-checks every
|
||||
description: 'The periodic qualify path (cron/operator on demand). Qualify-checks every
|
||||
|
||||
pending referral (bounded to 500 per sweep) and grants the ones that now
|
||||
|
||||
qualify. Global-admin only. Returns counts in the admin envelope.
|
||||
|
||||
'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
@@ -158,17 +167,20 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AdminSweepEnvelope'
|
||||
'403':
|
||||
$ref: '../shared/errors.yaml#/components/responses/Forbidden'
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '../shared/errors.yaml#/components/responses/InternalServerError'
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/v1/referrals/health:
|
||||
get:
|
||||
operationId: referralsHealth
|
||||
tags: [Health]
|
||||
tags:
|
||||
- Health
|
||||
summary: Liveness probe
|
||||
description: |
|
||||
Auto-registered liveness probe (serve.go registers `GET /v1/referrals/health`
|
||||
description: 'Auto-registered liveness probe (serve.go registers `GET /v1/referrals/health`
|
||||
|
||||
for every mounted subsystem). Response body is not defined by this package.
|
||||
|
||||
'
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
@@ -176,14 +188,19 @@ paths:
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
$ref: '../shared/auth.yaml#/components/securitySchemes/bearerAuth'
|
||||
$ref: ../shared/auth.yaml#/components/securitySchemes/bearerAuth
|
||||
schemas:
|
||||
ReferralStatus:
|
||||
type: string
|
||||
description: |
|
||||
A referral advances signed_up -> qualified -> credited. It never moves
|
||||
description: 'A referral advances signed_up -> qualified -> credited. It never moves
|
||||
|
||||
backward; credited is terminal.
|
||||
enum: [signed_up, qualified, credited]
|
||||
|
||||
'
|
||||
enum:
|
||||
- signed_up
|
||||
- qualified
|
||||
- credited
|
||||
MyReferralView:
|
||||
type: object
|
||||
description: One row in the referrer's own list (their side of the edge).
|
||||
@@ -253,7 +270,8 @@ components:
|
||||
$ref: '#/components/schemas/MyReferralView'
|
||||
ClaimRequest:
|
||||
type: object
|
||||
required: [code]
|
||||
required:
|
||||
- code
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
@@ -340,7 +358,8 @@ components:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok]
|
||||
enum:
|
||||
- ok
|
||||
msg:
|
||||
type: string
|
||||
data:
|
||||
@@ -360,8 +379,102 @@ components:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok]
|
||||
enum:
|
||||
- ok
|
||||
msg:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/AdminSweepData'
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- msg
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- error
|
||||
description: Always "error" for error responses
|
||||
msg:
|
||||
type: string
|
||||
description: Human-readable error message
|
||||
code:
|
||||
type: integer
|
||||
description: Application-specific error code
|
||||
data:
|
||||
type: object
|
||||
description: Additional error context
|
||||
data2:
|
||||
type: object
|
||||
description: Additional error details
|
||||
requestId:
|
||||
type: string
|
||||
description: Request ID for debugging
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Bad Request - Invalid input parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
invalidParameter:
|
||||
value:
|
||||
status: error
|
||||
msg: 'Invalid parameter: email format is incorrect'
|
||||
code: 400
|
||||
Unauthorized:
|
||||
description: Unauthorized - Authentication required
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
missingToken:
|
||||
value:
|
||||
status: error
|
||||
msg: Authentication required
|
||||
code: 401
|
||||
invalidToken:
|
||||
value:
|
||||
status: error
|
||||
msg: Invalid or expired token
|
||||
code: 401
|
||||
Forbidden:
|
||||
description: Forbidden - Insufficient permissions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
insufficientPermissions:
|
||||
value:
|
||||
status: error
|
||||
msg: You don't have permission to access this resource
|
||||
code: 403
|
||||
NotFound:
|
||||
description: Not Found - Resource does not exist
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
resourceNotFound:
|
||||
value:
|
||||
status: error
|
||||
msg: Resource not found
|
||||
code: 404
|
||||
InternalServerError:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
serverError:
|
||||
value:
|
||||
status: error
|
||||
msg: An internal error occurred
|
||||
code: 500
|
||||
requestId: req_abc123
|
||||
|
||||
Reference in New Issue
Block a user