mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[econ] influencer endpoint
This commit is contained in:
+52
-17
@@ -82,6 +82,7 @@ import {
|
|||||||
form,
|
form,
|
||||||
GameRewardRequest,
|
GameRewardRequest,
|
||||||
InfluencerIdsResponse,
|
InfluencerIdsResponse,
|
||||||
|
InfluencerTierResponse,
|
||||||
json,
|
json,
|
||||||
JsonArray,
|
JsonArray,
|
||||||
jsonBody,
|
jsonBody,
|
||||||
@@ -357,6 +358,13 @@ async function pushBalancePurchase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The influencer partner tier every account has here — the "not an influencer" one. It is
|
||||||
|
* the whole body of both `/api/influencerpartnerprogram/influencer` and `…/myinfluencer`,
|
||||||
|
* served as a bare number rather than wrapped in anything.
|
||||||
|
*/
|
||||||
|
const NOT_AN_INFLUENCER = 0
|
||||||
|
|
||||||
/** The operator-granted role that comes with a complimentary subscription. */
|
/** The operator-granted role that comes with a complimentary subscription. */
|
||||||
const DEVELOPER_ROLE = 'developer'
|
const DEVELOPER_ROLE = 'developer'
|
||||||
|
|
||||||
@@ -3233,27 +3241,26 @@ const app = new Hono<App>({ strict: false })
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whether the caller is in the influencer partner program. NOBODY is: this server runs
|
// One account's standing in the influencer partner program. NOBODY here has one: this
|
||||||
// no such program, and "not an influencer" is a 404 rather than a body saying so — the
|
// server runs no such program, so the answer is the literal `0` — the "not an influencer"
|
||||||
// reference answers 404 with an EMPTY body typed `application/json`, which is what the
|
// tier — for every account.
|
||||||
// client branches on. A 200 carrying null or `{}` is a different answer to it.
|
|
||||||
//
|
//
|
||||||
// Deliberately built by hand rather than through `c.notFound()`: the worker's not-found
|
// A BARE NUMBER is the whole body, like `…/makerai/checkfreetrialeligibility`'s bare
|
||||||
// handler answers its own body, and this has to be empty with that content type.
|
// `false`, not a number wrapped in an object. This used to answer 404 with an empty body;
|
||||||
|
// the tier is what the client actually reads.
|
||||||
//
|
//
|
||||||
// `accountId` is accepted and ignored — the reference binds it and never reads it, the
|
// `accountId` names the account being asked about. It makes no difference to the answer
|
||||||
// answer being the same for everyone. The token is still validated first, so an
|
// while nobody is an influencer, but it is read rather than ignored so this stays the
|
||||||
// unauthenticated caller gets 401 rather than the 404.
|
// question it looks like — the caller's own standing is `…/myinfluencer` below.
|
||||||
.get(
|
.get(
|
||||||
'/api/influencerpartnerprogram/influencer',
|
'/api/influencerpartnerprogram/influencer',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
tags: ['Econ'],
|
tags: ['Econ'],
|
||||||
summary: 'The caller’s influencer partner program status',
|
summary: 'An account’s influencer partner program tier',
|
||||||
description: [
|
description: [
|
||||||
'Always 404 with an EMPTY body typed `application/json` — this server runs no partner',
|
'The partner tier of the account named by `accountId`, as a BARE NUMBER — the whole',
|
||||||
'program, and 404 is how the reference says “not an influencer”. `accountId` is',
|
'body is `0`, not an object around it. Always 0: this server runs no partner program,',
|
||||||
'accepted and ignored; the answer is the same for every caller. Auth is checked first,',
|
'so no account is an influencer. Auth-gated; a missing or invalid token is a 401.',
|
||||||
'so a missing or invalid token is 401, not 404.',
|
|
||||||
].join(' '),
|
].join(' '),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
parameters: [
|
parameters: [
|
||||||
@@ -3261,19 +3268,47 @@ const app = new Hono<App>({ strict: false })
|
|||||||
name: 'accountId',
|
name: 'accountId',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
required: false,
|
required: false,
|
||||||
description: 'The account being asked about. Accepted and ignored.',
|
description: 'The account being asked about. Every account answers 0.',
|
||||||
schema: { type: 'integer' },
|
schema: { type: 'integer' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
responses: {
|
responses: {
|
||||||
404: { description: 'Not in the partner program — always. Empty body' },
|
200: json(InfluencerTierResponse, 'The account’s tier — always 0'),
|
||||||
401: UNAUTHORIZED_RESPONSE,
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(c)
|
if (id === null) return unauthorized(c)
|
||||||
return c.body('', 404, { 'Content-Type': 'application/json' })
|
return c.json(NOT_AN_INFLUENCER)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// The same question about the CALLER — the `my` form, which names no account because the
|
||||||
|
// token already does. Same bare `0`, for the same reason: nobody here is an influencer.
|
||||||
|
//
|
||||||
|
// Its own route rather than an alias of the one above, because the two differ in who they
|
||||||
|
// are about; they agree today only because the answer is currently the same for everyone.
|
||||||
|
.get(
|
||||||
|
'/api/influencerpartnerprogram/myinfluencer',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Econ'],
|
||||||
|
summary: 'The caller’s influencer partner program tier',
|
||||||
|
description: [
|
||||||
|
'The caller’s own partner tier — the `my` form of the route above, taking the account',
|
||||||
|
'from the token rather than a query parameter. A BARE NUMBER, always `0`: this server',
|
||||||
|
'runs no partner program. Auth-gated; a missing or invalid token is a 401.',
|
||||||
|
].join(' '),
|
||||||
|
security: AUTHED,
|
||||||
|
responses: {
|
||||||
|
200: json(InfluencerTierResponse, 'The caller’s tier — always 0'),
|
||||||
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const id = await authedId(c)
|
||||||
|
if (id === null) return unauthorized(c)
|
||||||
|
return c.json(NOT_AN_INFLUENCER)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,18 @@ export const InfluencerIdsResponse = z.object({
|
|||||||
.describe('Account ids in the partner program. Empty — no programme runs here'),
|
.describe('Account ids in the partner program. Empty — no programme runs here'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `GET /api/influencerpartnerprogram/influencer` and `…/myinfluencer` — one account's
|
||||||
|
* standing in the partner program.
|
||||||
|
*
|
||||||
|
* A BARE NUMBER, not an object: the body is the literal `0`, which is the "not an
|
||||||
|
* influencer" tier. Nobody on this server is one, so 0 is the answer for every account, the
|
||||||
|
* caller's own included.
|
||||||
|
*/
|
||||||
|
export const InfluencerTierResponse = z
|
||||||
|
.literal(0)
|
||||||
|
.describe('The account’s partner tier. Always 0 — nobody here is an influencer')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `GET /api/incentivizedreferrals/progress` — how far the caller has got with the
|
* `GET /api/incentivizedreferrals/progress` — how far the caller has got with the
|
||||||
* refer-a-friend rewards: how many referrals have been verified, and which rewards they
|
* refer-a-friend rewards: how many referrals have been verified, and which rewards they
|
||||||
|
|||||||
@@ -2448,9 +2448,8 @@ describe('econ endpoints', () => {
|
|||||||
{ headers: await bearer('207') }
|
{ headers: await bearer('207') }
|
||||||
)
|
)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
// An object around the list, not a bare array. Note this is a 200 while its
|
// An object around the list, not a bare array — unlike its single-account siblings
|
||||||
// single-account sibling below answers 404 — "nobody is" is a complete answer to
|
// below, whose whole body is a bare tier number.
|
||||||
// "who is?", where "are you?" is answered by the 404 itself.
|
|
||||||
expect(await res.json()).toEqual({ InfluencerIds: [] })
|
expect(await res.json()).toEqual({ InfluencerIds: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2459,24 +2458,43 @@ describe('econ endpoints', () => {
|
|||||||
expect(res.status).toBe(401)
|
expect(res.status).toBe(401)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/influencerpartnerprogram/influencer 404s with an empty JSON body', async () => {
|
test('GET /api/influencerpartnerprogram/influencer answers a bare 0', async () => {
|
||||||
const res = await exports.default.fetch(
|
const res = await exports.default.fetch(
|
||||||
|
`${ORIGIN}/api/influencerpartnerprogram/influencer?accountId=220`,
|
||||||
|
{ headers: await bearer('206') }
|
||||||
|
)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
// The tier is the WHOLE body — a bare number, not `{ Tier: 0 }` or a string. 0 is
|
||||||
|
// "not an influencer", which every account is here.
|
||||||
|
expect(res.headers.get('content-type')).toContain('application/json')
|
||||||
|
expect(await res.text()).toBe('0')
|
||||||
|
|
||||||
|
// Any account, the caller's own included, gets the same answer.
|
||||||
|
const self = await exports.default.fetch(
|
||||||
`${ORIGIN}/api/influencerpartnerprogram/influencer?accountId=206`,
|
`${ORIGIN}/api/influencerpartnerprogram/influencer?accountId=206`,
|
||||||
{ headers: await bearer('206') }
|
{ headers: await bearer('206') }
|
||||||
)
|
)
|
||||||
// 404 IS the answer — "not an influencer" — and the body is empty, not `{}` or null,
|
expect(await self.json()).toBe(0)
|
||||||
// with the content type the reference sends.
|
|
||||||
expect(res.status).toBe(404)
|
|
||||||
expect(res.headers.get('content-type')).toContain('application/json')
|
|
||||||
expect(await res.text()).toBe('')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/influencerpartnerprogram/influencer 401s without a bearer token', async () => {
|
test('GET /api/influencerpartnerprogram/myinfluencer answers a bare 0', async () => {
|
||||||
// Auth is checked before the 404, so an unauthenticated caller is told that, not that
|
// The `my` form takes the account from the token instead of a query parameter, and
|
||||||
// they aren't an influencer.
|
// answers the same tier in the same shape.
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/influencerpartnerprogram/influencer`)
|
const res = await exports.default.fetch(`${ORIGIN}/api/influencerpartnerprogram/myinfluencer`, {
|
||||||
expect(res.status).toBe(401)
|
headers: await bearer('206'),
|
||||||
expect(await res.text()).toBe('')
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe('0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the influencer tier routes 401 without a bearer token', async () => {
|
||||||
|
// Auth is checked before anything is answered, so an unauthenticated caller is told
|
||||||
|
// that rather than handed a tier.
|
||||||
|
for (const path of ['influencer', 'myinfluencer']) {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/influencerpartnerprogram/${path}`)
|
||||||
|
expect(res.status, path).toBe(401)
|
||||||
|
expect(await res.text()).toBe('')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/makerai/checkfreetrialeligibility answers a bare false', async () => {
|
test('GET /api/makerai/checkfreetrialeligibility answers a bare false', async () => {
|
||||||
@@ -2592,6 +2610,7 @@ describe('econ endpoints', () => {
|
|||||||
'GET /api/incentivizedreferrals/progress',
|
'GET /api/incentivizedreferrals/progress',
|
||||||
'GET /api/influencerpartnerprogram/influencer',
|
'GET /api/influencerpartnerprogram/influencer',
|
||||||
'GET /api/influencerpartnerprogram/influencers',
|
'GET /api/influencerpartnerprogram/influencers',
|
||||||
|
'GET /api/influencerpartnerprogram/myinfluencer',
|
||||||
'GET /api/itemWishlists/v1/wishlist/me',
|
'GET /api/itemWishlists/v1/wishlist/me',
|
||||||
'GET /api/itemWishlists/v1/wishlist/{accountId}',
|
'GET /api/itemWishlists/v1/wishlist/{accountId}',
|
||||||
'GET /api/makerai/checkfreetrialeligibility',
|
'GET /api/makerai/checkfreetrialeligibility',
|
||||||
|
|||||||
Reference in New Issue
Block a user