mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-26 23:48:52 -07:00
[commerce] stub out commerce, for historical purposes
This commit is contained in:
@@ -1,9 +1,21 @@
|
||||
import { Hono } from 'hono'
|
||||
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import catalog from '../static/catalog-v1-all.json'
|
||||
import {
|
||||
BareBoolean,
|
||||
boolQuery,
|
||||
CatalogSku,
|
||||
HealthResponse,
|
||||
InitiatePurchaseRequest,
|
||||
InitiatePurchaseResponse,
|
||||
json,
|
||||
JsonArray,
|
||||
jsonBody,
|
||||
} from './openapi'
|
||||
|
||||
import type { App } from './context'
|
||||
|
||||
@@ -11,6 +23,14 @@ import type { App } from './context'
|
||||
* Commerce routes. The `commerce` prefix maps to this worker's subdomain, so
|
||||
* method routes are served bare.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The transaction id every purchase initiation answers with. Real money never changes
|
||||
* hands here and nothing is persisted, so the client only needs a well-formed handle to
|
||||
* carry through the rest of its store flow.
|
||||
*/
|
||||
const PLACEHOLDER_TRANSACTION_ID = 1234567890
|
||||
|
||||
const app = new Hono<App>()
|
||||
.use(
|
||||
'*',
|
||||
@@ -25,24 +45,131 @@ const app = new Hono<App>()
|
||||
.onError(withOnError())
|
||||
.notFound(withNotFound())
|
||||
|
||||
.get('/', (c) => c.json({ service: 'commerce', status: 'ok' }))
|
||||
.get(
|
||||
'/',
|
||||
describeRoute({
|
||||
tags: ['Service'],
|
||||
summary: 'Health check',
|
||||
description: 'Liveness probe for the commerce worker. No auth.',
|
||||
responses: { 200: json(HealthResponse, 'Service is up') },
|
||||
}),
|
||||
(c) => c.json({ service: 'commerce', status: 'ok' })
|
||||
)
|
||||
|
||||
// Whether the player has ever spent money. A 404 here makes the client treat
|
||||
// it as an error, so we return `false` (no purchases).
|
||||
.get('/purchase/v1/hasspentmoney', (c) => c.json(false))
|
||||
.get(
|
||||
'/purchase/v1/hasspentmoney',
|
||||
describeRoute({
|
||||
tags: ['Purchase'],
|
||||
summary: 'Whether the player has ever spent money',
|
||||
description: [
|
||||
'Always `false` — nobody buys anything on this server. A 404 here makes the client',
|
||||
'treat the call as an error, so the answer is the bare boolean rather than nothing.',
|
||||
].join(' '),
|
||||
responses: { 200: json(BareBoolean, 'Always false (no purchases)') },
|
||||
}),
|
||||
(c) => c.json(false)
|
||||
)
|
||||
|
||||
// Begin a purchase. The client asks for a transaction handle before it takes the
|
||||
// player to the platform store; nothing is charged or recorded here, so the id is a
|
||||
// fixed placeholder and the posted body is ignored.
|
||||
.post(
|
||||
'/purchase/v1/initiatepurchase',
|
||||
describeRoute({
|
||||
tags: ['Purchase'],
|
||||
summary: 'Begin a purchase',
|
||||
description: [
|
||||
'Hands the client the transaction handle it carries through the rest of the store',
|
||||
'flow. Nothing is charged and no transaction is recorded, so the id is a fixed',
|
||||
'placeholder and the posted body is accepted and ignored — an absent or unparseable',
|
||||
'body is a 200, not a 400.',
|
||||
].join(' '),
|
||||
requestBody: jsonBody(InitiatePurchaseRequest, 'The purchase the player confirmed'),
|
||||
responses: { 200: json(InitiatePurchaseResponse, 'The (placeholder) transaction id') },
|
||||
}),
|
||||
(c) => c.json({ transactionId: PLACEHOLDER_TRANSACTION_ID })
|
||||
)
|
||||
|
||||
// The purchasable SKU catalog (token packs, special offers), served from the
|
||||
// bundled static JSON. The client passes `?onlyAvailableSkus=true`; the bundled
|
||||
// catalog is already only the available SKUs, so the param doesn't change the
|
||||
// response.
|
||||
.get('/api/catalog/v1/all', (c) => c.json(catalog))
|
||||
.get(
|
||||
'/api/catalog/v1/all',
|
||||
describeRoute({
|
||||
tags: ['Catalog'],
|
||||
summary: 'The purchasable SKU catalog',
|
||||
description: [
|
||||
'The token packs, bundles and special offers the store shows, served from the bundled',
|
||||
'static catalog. The client’s `onlyAvailableSkus` is accepted and ignored: the bundled',
|
||||
'catalog already contains only available SKUs.',
|
||||
].join(' '),
|
||||
parameters: [
|
||||
boolQuery('onlyAvailableSkus', 'Accepted and ignored — the catalog is already filtered'),
|
||||
],
|
||||
responses: { 200: json(CatalogSku.array(), 'Every available SKU') },
|
||||
}),
|
||||
(c) => c.json(catalog)
|
||||
)
|
||||
|
||||
// Current purchase campaigns (limited-time offers/promos). None exist, and
|
||||
// an empty list is the client's "no active campaigns" state.
|
||||
.get('/purchasecampaign/allcurrent/v2', (c) => c.json([]))
|
||||
.get(
|
||||
'/purchasecampaign/allcurrent/v2',
|
||||
describeRoute({
|
||||
tags: ['Purchase'],
|
||||
summary: 'Current purchase campaigns',
|
||||
description: [
|
||||
'Limited-time offers and promos. Always `[]` — none exist, and an empty list is the',
|
||||
'client’s “no active campaigns” state.',
|
||||
].join(' '),
|
||||
responses: { 200: json(JsonArray, 'Always empty (no active campaigns)') },
|
||||
}),
|
||||
(c) => c.json([])
|
||||
)
|
||||
|
||||
// Token-bundle purchase reminders (the "buy more tokens" nudge). None to show,
|
||||
// and an empty list is the client's "no reminders" state.
|
||||
.get('/reminder/currentTokenBundles/v2', (c) => c.json([]))
|
||||
.get(
|
||||
'/reminder/currentTokenBundles/v2',
|
||||
describeRoute({
|
||||
tags: ['Purchase'],
|
||||
summary: 'Token-bundle purchase reminders',
|
||||
description: [
|
||||
'The “buy more tokens” nudges. Always `[]` — there are none to show, and an empty list',
|
||||
'is the client’s “no reminders” state.',
|
||||
].join(' '),
|
||||
responses: { 200: json(JsonArray, 'Always empty (no reminders)') },
|
||||
}),
|
||||
(c) => c.json([])
|
||||
)
|
||||
|
||||
// The generated spec. Documentation only — no request is validated against it (see
|
||||
// openapi.ts). `hide: true` keeps this route out of its own output.
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare commerce',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'The store surface for recflare, a private-server reimplementation of the Rec Room',
|
||||
'backend: the SKU catalog the client shows and the purchase calls it makes around it.',
|
||||
'',
|
||||
'No money moves here. There is no store integration and no purchase storage, so the',
|
||||
'catalog is a bundled static asset, the campaign and reminder feeds are empty, and a',
|
||||
'purchase initiation answers with a placeholder transaction id.',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://commerce.recflare.net', description: 'Production' }],
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export default app
|
||||
|
||||
Reference in New Issue
Block a user