@hearthkit/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +45 -0
- package/src/cli-contract.ts +423 -0
- package/src/cli-failure-results.ts +175 -0
- package/src/cli-output-streams.ts +14 -0
- package/src/cli-runtime-context.ts +8 -0
- package/src/default-backup-file-path.ts +28 -0
- package/src/derive-hearthkit-project-name.ts +27 -0
- package/src/derive-local-storage-bucket-name.test.ts +89 -0
- package/src/derive-local-storage-bucket-name.ts +24 -0
- package/src/docker-compose-commands.ts +100 -0
- package/src/format-doctor-report.ts +25 -0
- package/src/generate-local-infra-compose.test.ts +215 -0
- package/src/generate-local-infra-compose.ts +154 -0
- package/src/hearthkit-bin-entry.js +19 -0
- package/src/hearthkit-bin-execution.test.ts +136 -0
- package/src/hearthkit-bin.ts +8 -0
- package/src/index.ts +107 -0
- package/src/load-payments-catalog-module.ts +60 -0
- package/src/parse-cli-invocation.ts +365 -0
- package/src/read-environment-variable-value.ts +14 -0
- package/src/read-project-infra-manifest.ts +99 -0
- package/src/report-cli-outcome.ts +139 -0
- package/src/resolve-admin-database-url.ts +72 -0
- package/src/resolve-local-infra-compose-file.ts +80 -0
- package/src/run-child-process-command.ts +90 -0
- package/src/run-db-lifecycle-command.ts +103 -0
- package/src/run-dev-command.ts +104 -0
- package/src/run-dev-infra-command.ts +115 -0
- package/src/run-doctor-checks.ts +339 -0
- package/src/run-hearthkit-cli-db-commands.test.ts +330 -0
- package/src/run-hearthkit-cli-dev-infra-bucket.test.ts +254 -0
- package/src/run-hearthkit-cli-dev-infra.test.ts +616 -0
- package/src/run-hearthkit-cli-doctor.test.ts +75 -0
- package/src/run-hearthkit-cli-payments-sync.test.ts +180 -0
- package/src/run-hearthkit-cli.ts +62 -0
- package/src/run-payments-sync-command.ts +113 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
expectCliFailure,
|
|
5
|
+
expectCliSuccess,
|
|
6
|
+
runHearthkitCliGate,
|
|
7
|
+
singleStandardOutputLine,
|
|
8
|
+
} from '../test-fixtures/cli-run-expectations.ts'
|
|
9
|
+
import {
|
|
10
|
+
createGateDirectory,
|
|
11
|
+
removeGateDirectory,
|
|
12
|
+
} from '../test-fixtures/gate-project-directories.ts'
|
|
13
|
+
import { loadHearthkitCliPaymentsExports } from '../test-fixtures/hearthkit-cli-payments-exports.ts'
|
|
14
|
+
import {
|
|
15
|
+
uniqueGateCliCatalogNames,
|
|
16
|
+
writeGateCliCatalogFile,
|
|
17
|
+
writeGateCliUnusableCatalogFile,
|
|
18
|
+
type GateCliCatalogNames,
|
|
19
|
+
} from '../test-fixtures/payments-sync-gate-catalog.ts'
|
|
20
|
+
import {
|
|
21
|
+
archiveGateStripeSyncedPrices,
|
|
22
|
+
assertGateStripeSyncWasTestMode,
|
|
23
|
+
readGateStripeSecretKey,
|
|
24
|
+
} from '../test-fixtures/stripe-gate-secret-key.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* hearthkit payments sync. One live gate against Stripe test mode, and the three file and
|
|
28
|
+
* environment paths that reach no service at all.
|
|
29
|
+
*
|
|
30
|
+
* The command is specified to read exactly one environment variable, so every gate here hands it an
|
|
31
|
+
* environment holding only that variable or nothing. It opens no Postgres connection either — the
|
|
32
|
+
* CLI builds the payments client with a database URL nothing listens on — so a sync that started
|
|
33
|
+
* querying would fail these gates rather than pass them.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Prices this run created in the shared test-mode Stripe account, archived in afterAll. */
|
|
37
|
+
const createdStripePrices: { stripePriceId: unknown; stripeProductId: unknown }[] = []
|
|
38
|
+
|
|
39
|
+
let gateDirectoryPath: string
|
|
40
|
+
let liveCatalogNames: GateCliCatalogNames
|
|
41
|
+
let gateStripeSecretKey: string | undefined
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
gateDirectoryPath = await createGateDirectory('payments-sync')
|
|
45
|
+
liveCatalogNames = uniqueGateCliCatalogNames('sync')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
if (gateStripeSecretKey !== undefined && createdStripePrices.length > 0) {
|
|
50
|
+
await archiveGateStripeSyncedPrices(gateStripeSecretKey, createdStripePrices)
|
|
51
|
+
}
|
|
52
|
+
await removeGateDirectory(gateDirectoryPath)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('hearthkit payments sync', () => {
|
|
56
|
+
it('pushes the project catalog to Stripe test mode and reports one created price per catalog price', async () => {
|
|
57
|
+
const { cliPaymentsSyncCompleteLinePrefix, defaultPaymentsCatalogPath } =
|
|
58
|
+
await loadHearthkitCliPaymentsExports()
|
|
59
|
+
// Not skipped when the key is absent: this is the only gate that proves the command reaches
|
|
60
|
+
// Stripe at all, so a missing key has to fail loudly. The helper names both places to put it.
|
|
61
|
+
gateStripeSecretKey = await readGateStripeSecretKey()
|
|
62
|
+
const catalogFilePath = await writeGateCliCatalogFile({
|
|
63
|
+
directoryPath: gateDirectoryPath,
|
|
64
|
+
fileName: 'payments-catalog.ts',
|
|
65
|
+
catalogNames: liveCatalogNames,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const run = await runHearthkitCliGate({
|
|
69
|
+
argv: ['payments', 'sync'],
|
|
70
|
+
cwd: gateDirectoryPath,
|
|
71
|
+
// Only STRIPE_SECRET_KEY, because the contract says the command reads nothing else.
|
|
72
|
+
env: { STRIPE_SECRET_KEY: gateStripeSecretKey },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const success = expectCliSuccess(run, 'payments-sync-command-succeeded', 0)
|
|
76
|
+
// The measured test-mode guard, before this run's objects are touched again for any reason.
|
|
77
|
+
assertGateStripeSyncWasTestMode(success.stripeLivemode)
|
|
78
|
+
expect(success.stripeLivemode).toBe(false)
|
|
79
|
+
createdStripePrices.push(...success.syncedPrices)
|
|
80
|
+
|
|
81
|
+
// No --catalog flag, so the path is the default resolved against cwd.
|
|
82
|
+
expect(success.catalogPath).toBe(resolve(gateDirectoryPath, defaultPaymentsCatalogPath))
|
|
83
|
+
expect(success.catalogPath).toBe(catalogFilePath)
|
|
84
|
+
expect([...success.syncedPrices].map((price) => String(price.priceName)).toSorted()).toEqual(
|
|
85
|
+
[liveCatalogNames.subscriptionPriceName, liveCatalogNames.oneTimePriceName].toSorted(),
|
|
86
|
+
)
|
|
87
|
+
// Every price name is unique to this run, so the first sync of it can only create.
|
|
88
|
+
expect(success.syncedPrices.every((price) => price.syncAction === 'created')).toBe(true)
|
|
89
|
+
expect(success.createdPriceCount).toBe(success.syncedPrices.length)
|
|
90
|
+
expect(success.replacedPriceCount).toBe(0)
|
|
91
|
+
expect(success.unchangedPriceCount).toBe(0)
|
|
92
|
+
expect(
|
|
93
|
+
success.createdPriceCount + success.replacedPriceCount + success.unchangedPriceCount,
|
|
94
|
+
).toBe(success.syncedPrices.length)
|
|
95
|
+
|
|
96
|
+
// One machine-readable line on stdout; only its prefix and the counts it carries are contract.
|
|
97
|
+
const printedLine = singleStandardOutputLine(run)
|
|
98
|
+
expect(printedLine.startsWith(cliPaymentsSyncCompleteLinePrefix)).toBe(true)
|
|
99
|
+
expect(printedLine).toContain(String(success.createdPriceCount))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('fails with cli-payments-catalog-not-found and the absolute default path when the project has no catalog file', async () => {
|
|
103
|
+
const { cliPaymentsCatalogNotFoundErrorPrefix, defaultPaymentsCatalogPath } =
|
|
104
|
+
await loadHearthkitCliPaymentsExports()
|
|
105
|
+
// A directory of its own, so the live gate's catalog file cannot make this one pass.
|
|
106
|
+
const emptyDirectoryPath = await createGateDirectory('payments-sync-empty')
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// No Stripe key at all: the file check comes first, so this path needs no service and no key.
|
|
110
|
+
const run = await runHearthkitCliGate({
|
|
111
|
+
argv: ['payments', 'sync'],
|
|
112
|
+
cwd: emptyDirectoryPath,
|
|
113
|
+
env: {},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const failure = expectCliFailure(run, 'cli-payments-catalog-not-found', 1)
|
|
117
|
+
expect(isAbsolute(failure.catalogPath)).toBe(true)
|
|
118
|
+
expect(failure.catalogPath).toBe(resolve(emptyDirectoryPath, defaultPaymentsCatalogPath))
|
|
119
|
+
expect(failure.message.startsWith(cliPaymentsCatalogNotFoundErrorPrefix)).toBe(true)
|
|
120
|
+
expect(run.standardError).toContain(failure.message)
|
|
121
|
+
expect(run.standardOutput.trim()).toBe('')
|
|
122
|
+
} finally {
|
|
123
|
+
await removeGateDirectory(emptyDirectoryPath)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('fails with cli-payments-catalog-unloadable when the --catalog file exists but exports no catalog', async () => {
|
|
128
|
+
const { cliPaymentsCatalogUnloadableErrorPrefix } = await loadHearthkitCliPaymentsExports()
|
|
129
|
+
const unusableCatalogFileName = 'gate-unusable-catalog.ts'
|
|
130
|
+
const unusableCatalogFilePath = await writeGateCliUnusableCatalogFile({
|
|
131
|
+
directoryPath: gateDirectoryPath,
|
|
132
|
+
fileName: unusableCatalogFileName,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const run = await runHearthkitCliGate({
|
|
136
|
+
argv: ['payments', 'sync', '--catalog', `./${unusableCatalogFileName}`],
|
|
137
|
+
cwd: gateDirectoryPath,
|
|
138
|
+
env: {},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const failure = expectCliFailure(run, 'cli-payments-catalog-unloadable', 1)
|
|
142
|
+
// The file is there, so this is not "not found", and the flag value is resolved against cwd.
|
|
143
|
+
expect(failure.catalogPath).toBe(unusableCatalogFilePath)
|
|
144
|
+
expect(failure.loadFailureDetail.length).toBeGreaterThan(0)
|
|
145
|
+
expect(failure.message.startsWith(cliPaymentsCatalogUnloadableErrorPrefix)).toBe(true)
|
|
146
|
+
expect(run.standardError).toContain(failure.message)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('wraps the payments failure as cli-payments-sync-failed when STRIPE_SECRET_KEY is unset', async () => {
|
|
150
|
+
const { cliPaymentsSyncFailedErrorPrefix, stripeSecretKeyEnvVariableName } =
|
|
151
|
+
await loadHearthkitCliPaymentsExports()
|
|
152
|
+
const catalogNames = uniqueGateCliCatalogNames('unset-key')
|
|
153
|
+
const unsetKeyCatalogFileName = 'gate-unset-key-catalog.ts'
|
|
154
|
+
await writeGateCliCatalogFile({
|
|
155
|
+
directoryPath: gateDirectoryPath,
|
|
156
|
+
fileName: unsetKeyCatalogFileName,
|
|
157
|
+
catalogNames,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// The catalog loads, so the run gets as far as createPaymentsClient, which rejects the env
|
|
161
|
+
// object. A missing key is deliberately not a CLI failure kind; it arrives wrapped.
|
|
162
|
+
const run = await runHearthkitCliGate({
|
|
163
|
+
argv: ['payments', 'sync', '--catalog', `./${unsetKeyCatalogFileName}`],
|
|
164
|
+
cwd: gateDirectoryPath,
|
|
165
|
+
env: {},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const failure = expectCliFailure(run, 'cli-payments-sync-failed', 1)
|
|
169
|
+
expect(failure.paymentsFailure.kind).toBe('payments-input-invalid')
|
|
170
|
+
if (failure.paymentsFailure.kind === 'payments-input-invalid') {
|
|
171
|
+
expect(failure.paymentsFailure.invalidFieldName).toBe('payments-env')
|
|
172
|
+
}
|
|
173
|
+
expect(failure.message.startsWith(cliPaymentsSyncFailedErrorPrefix)).toBe(true)
|
|
174
|
+
// Both prefixes stay greppable: the payments message follows the CLI prefix unchanged.
|
|
175
|
+
expect(failure.message).toContain(failure.paymentsFailure.message)
|
|
176
|
+
// The payments message names the field, not the variable, so the CLI's guidance names it.
|
|
177
|
+
expect(run.standardError).toContain(stripeSecretKeyEnvVariableName)
|
|
178
|
+
expect(run.standardError).toContain(failure.message)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CliCommandInvocation, CliCommandResult, RunHearthkitCli } from './cli-contract.ts'
|
|
2
|
+
import type { CliRuntimeContext } from './cli-runtime-context.ts'
|
|
3
|
+
import { parseCliInvocation } from './parse-cli-invocation.ts'
|
|
4
|
+
import { reportCliOutcome } from './report-cli-outcome.ts'
|
|
5
|
+
import { runDbLifecycleCommand } from './run-db-lifecycle-command.ts'
|
|
6
|
+
import { runDevCommand } from './run-dev-command.ts'
|
|
7
|
+
import { runDevInfraDownCommand, runDevInfraUpCommand } from './run-dev-infra-command.ts'
|
|
8
|
+
import { runDoctorCommand } from './run-doctor-checks.ts'
|
|
9
|
+
import { runPaymentsSyncCommand } from './run-payments-sync-command.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs one hearthkit command end to end: resolve the working directory and environment, parse argv,
|
|
13
|
+
* hand the invocation to the package that owns the work, then print and score the outcome. Every
|
|
14
|
+
* contract failure comes back as a result with an exit code; nothing here throws to report one.
|
|
15
|
+
*/
|
|
16
|
+
export const runHearthkitCli: RunHearthkitCli = async (options) => {
|
|
17
|
+
const context: CliRuntimeContext = {
|
|
18
|
+
workingDirectoryPath: options.cwd ?? process.cwd(),
|
|
19
|
+
environmentVariables: options.env ?? process.env,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parse = parseCliInvocation({ argv: options.argv, context })
|
|
23
|
+
if (parse.kind === 'cli-invocation-rejected') {
|
|
24
|
+
return {
|
|
25
|
+
exitCode: reportCliOutcome({ result: parse.failure, invocation: undefined }),
|
|
26
|
+
result: parse.failure,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await runCliCommandInvocation({ invocation: parse.invocation, context })
|
|
31
|
+
return {
|
|
32
|
+
exitCode: reportCliOutcome({ result, invocation: parse.invocation }),
|
|
33
|
+
result,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Dispatches a parsed invocation to the one handler that owns it; the switch is the whole command registry. */
|
|
38
|
+
async function runCliCommandInvocation(options: {
|
|
39
|
+
invocation: CliCommandInvocation
|
|
40
|
+
context: CliRuntimeContext
|
|
41
|
+
}): Promise<CliCommandResult> {
|
|
42
|
+
const { invocation, context } = options
|
|
43
|
+
|
|
44
|
+
if (invocation.commandPath === 'dev') {
|
|
45
|
+
return runDevCommand(context)
|
|
46
|
+
}
|
|
47
|
+
if (invocation.commandPath === 'dev infra up') {
|
|
48
|
+
return runDevInfraUpCommand(context)
|
|
49
|
+
}
|
|
50
|
+
if (invocation.commandPath === 'dev infra down') {
|
|
51
|
+
return runDevInfraDownCommand(context)
|
|
52
|
+
}
|
|
53
|
+
if (invocation.commandPath === 'doctor') {
|
|
54
|
+
return runDoctorCommand(context)
|
|
55
|
+
}
|
|
56
|
+
if (invocation.commandPath === 'payments sync') {
|
|
57
|
+
return runPaymentsSyncCommand({ catalogPath: invocation.catalogPath, context })
|
|
58
|
+
}
|
|
59
|
+
// Everything left is a db command, and it has to be: a command path added to the enum without a
|
|
60
|
+
// handler here fails to typecheck against DbLifecycleInvocation rather than falling through.
|
|
61
|
+
return runDbLifecycleCommand({ invocation, context })
|
|
62
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createDrizzleClient, postgresConnectionStringSchema } from '@hearthkit/db'
|
|
2
|
+
import type { PaymentsCatalog, PaymentsEnvValues } from '@hearthkit/payments/payments-contract'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { stripeSecretKeyEnvVariableName, type CliCommandResult } from './cli-contract.ts'
|
|
5
|
+
import {
|
|
6
|
+
paymentsCatalogNotFoundFailure,
|
|
7
|
+
paymentsCatalogUnloadableFailure,
|
|
8
|
+
paymentsSyncFailedFailure,
|
|
9
|
+
} from './cli-failure-results.ts'
|
|
10
|
+
import type { CliRuntimeContext } from './cli-runtime-context.ts'
|
|
11
|
+
import { loadPaymentsCatalogModule } from './load-payments-catalog-module.ts'
|
|
12
|
+
import { readEnvironmentVariableValue } from './read-environment-variable-value.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* hearthkit payments sync: resolve the catalog file, hand it to @hearthkit/payments, map the result.
|
|
16
|
+
*
|
|
17
|
+
* The file checks come first so a project with no catalog fails without a Stripe key. Everything
|
|
18
|
+
* after that belongs to payments: the catalog value is passed on unvalidated, and a missing
|
|
19
|
+
* STRIPE_SECRET_KEY is not a CLI failure kind — it arrives as the payments env failure, wrapped.
|
|
20
|
+
*
|
|
21
|
+
* @hearthkit/payments is imported inside the function rather than at the top of the file, so the one
|
|
22
|
+
* command that syncs a catalog is the only one that pays for loading the Stripe SDK. Every other
|
|
23
|
+
* command, and the bin's own startup, stays free of it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Sync verifies no signature, runs no query and writes no customer row, so the CLI supplies the rest
|
|
27
|
+
// of the payments client itself rather than asking the operator for it: a placeholder webhook secret,
|
|
28
|
+
// a Drizzle client aimed at a port nothing listens on, and organizationsEnabled false. A sync that
|
|
29
|
+
// ever grew a query would report a wrapped payments-database-unavailable rather than touch a real
|
|
30
|
+
// database, which is the direction this is meant to fail in.
|
|
31
|
+
/** Webhook secret handed to createPaymentsClient because its env fragment requires one; sync never verifies a signature. */
|
|
32
|
+
const unusedWebhookSecretPlaceholder = 'hearthkit-cli-payments-sync-unused-webhook-secret'
|
|
33
|
+
|
|
34
|
+
/** Connection string the throwaway Drizzle client points at; a pool opens no connection until a query runs, and sync runs none. */
|
|
35
|
+
const unusedProjectDatabaseUrl = postgresConnectionStringSchema.parse(
|
|
36
|
+
'postgresql://hearthkit:hearthkit@127.0.0.1:1/hearthkit-cli-payments-sync-unused',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Both of these pass a value straight through with the type @hearthkit/payments declares and no check
|
|
40
|
+
// of their own. That is the contract: createPaymentsClient is what validates a catalog and an env
|
|
41
|
+
// object, so a bad catalog comes back as payments-catalog-invalid and an unset key as
|
|
42
|
+
// payments-input-invalid with payments-env, rather than as a second opinion formed here.
|
|
43
|
+
/** Passes the catalog module's export through unvalidated, because createPaymentsClient validates it. */
|
|
44
|
+
const unvalidatedPaymentsCatalogSchema = z.custom<PaymentsCatalog>()
|
|
45
|
+
|
|
46
|
+
/** Passes the env object through unvalidated, because createPaymentsClient rejects a missing key as payments-env. */
|
|
47
|
+
const unvalidatedPaymentsEnvSchema = z.custom<PaymentsEnvValues>()
|
|
48
|
+
|
|
49
|
+
/** Runs one payments sync invocation and returns the success shape or the CLI failure that wraps what payments said. */
|
|
50
|
+
export async function runPaymentsSyncCommand(options: {
|
|
51
|
+
catalogPath: string
|
|
52
|
+
context: CliRuntimeContext
|
|
53
|
+
}): Promise<CliCommandResult> {
|
|
54
|
+
const { catalogPath, context } = options
|
|
55
|
+
|
|
56
|
+
const catalogLoad = await loadPaymentsCatalogModule(catalogPath)
|
|
57
|
+
if (catalogLoad.kind === 'payments-catalog-file-absent') {
|
|
58
|
+
return paymentsCatalogNotFoundFailure(catalogPath)
|
|
59
|
+
}
|
|
60
|
+
if (catalogLoad.kind === 'payments-catalog-unloadable') {
|
|
61
|
+
return paymentsCatalogUnloadableFailure(catalogPath, catalogLoad.loadFailureDetail)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { createPaymentsClient, syncPaymentsCatalog } = await import('@hearthkit/payments')
|
|
65
|
+
|
|
66
|
+
const stripeSecretKey =
|
|
67
|
+
readEnvironmentVariableValue(context.environmentVariables, stripeSecretKeyEnvVariableName) ?? ''
|
|
68
|
+
|
|
69
|
+
const { drizzleClient, closeDatabaseClient } = createDrizzleClient<Record<string, unknown>>({
|
|
70
|
+
databaseUrl: unusedProjectDatabaseUrl,
|
|
71
|
+
schema: {},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const clientResult = createPaymentsClient({
|
|
76
|
+
paymentsEnv: unvalidatedPaymentsEnvSchema.parse({
|
|
77
|
+
STRIPE_SECRET_KEY: stripeSecretKey,
|
|
78
|
+
STRIPE_WEBHOOK_SECRET: unusedWebhookSecretPlaceholder,
|
|
79
|
+
}),
|
|
80
|
+
drizzleClient,
|
|
81
|
+
paymentsCatalog: unvalidatedPaymentsCatalogSchema.parse(catalogLoad.catalogValue),
|
|
82
|
+
organizationsEnabled: false,
|
|
83
|
+
})
|
|
84
|
+
if (clientResult.kind !== 'payments-client-created') {
|
|
85
|
+
return paymentsSyncFailedFailure(clientResult)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const syncResult = await syncPaymentsCatalog({ paymentsClient: clientResult.paymentsClient })
|
|
89
|
+
if (syncResult.kind !== 'payments-catalog-synced') {
|
|
90
|
+
return paymentsSyncFailedFailure(syncResult)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
kind: 'payments-sync-command-succeeded',
|
|
95
|
+
catalogPath,
|
|
96
|
+
syncedPrices: syncResult.syncedPrices,
|
|
97
|
+
createdPriceCount: countPricesSyncedBy(syncResult.syncedPrices, 'created'),
|
|
98
|
+
replacedPriceCount: countPricesSyncedBy(syncResult.syncedPrices, 'replaced'),
|
|
99
|
+
unchangedPriceCount: countPricesSyncedBy(syncResult.syncedPrices, 'unchanged'),
|
|
100
|
+
stripeLivemode: syncResult.stripeLivemode,
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
await closeDatabaseClient()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** How many synced prices took one action; the three words are payments' own syncAction values. */
|
|
108
|
+
function countPricesSyncedBy(
|
|
109
|
+
syncedPrices: readonly { syncAction: string }[],
|
|
110
|
+
syncAction: string,
|
|
111
|
+
): number {
|
|
112
|
+
return syncedPrices.filter((syncedPrice) => syncedPrice.syncAction === syncAction).length
|
|
113
|
+
}
|