Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
124e928
feat(cutover): add cash wallet migration state primitives
forge0x May 19, 2026
233f49d
feat(cutover): persist cash wallet cutover state
forge0x May 19, 2026
bc0e079
feat(cutover): add cash wallet write guard
forge0x May 19, 2026
a876407
feat(cutover): classify cash wallet migration candidates
forge0x May 20, 2026
852fe79
feat(cutover): add cash wallet preflight summary
forge0x May 20, 2026
c79aff7
feat(cutover): collect cash wallet discovery results
forge0x May 20, 2026
9f2a8e5
feat(cutover): plan primary cash wallet migrations
forge0x May 20, 2026
f5c29c9
feat(cutover): upsert planned migration records
forge0x May 20, 2026
5e9523c
feat(cutover): prepare primary migration batch
forge0x May 20, 2026
b40d28e
feat(cutover): start migration worker checkpoint
forge0x May 20, 2026
5f4827d
feat(cutover): record migration source balance
forge0x May 20, 2026
b2dd547
feat(cutover): create balance move invoice checkpoint
forge0x May 20, 2026
4d78c2c
feat(cutover): send balance move payment checkpoint
forge0x May 20, 2026
eaa7c55
feat(cutover): verify balance move checkpoint
forge0x May 20, 2026
9a7a723
feat(cutover): create fee reimbursement invoice checkpoint
forge0x May 20, 2026
be7b689
feat(cutover): complete fee reimbursement checkpoint
forge0x May 20, 2026
cede280
feat(cutover): flip default wallet checkpoint
forge0x May 20, 2026
1f05ef0
feat(cutover): complete migration worker checkpoints
forge0x May 20, 2026
50cf194
feat(cutover): provision destination checkpoint
forge0x May 20, 2026
f9014f5
feat(cutover): dispatch migration worker steps
forge0x May 20, 2026
b808322
feat(cutover): run locked migration batches
forge0x May 20, 2026
7534f78
feat(cutover): build migration step handlers
forge0x May 20, 2026
af6c701
feat(cutover): wire migration runtime services
forge0x May 20, 2026
a1b5016
feat(cutover): orchestrate primary migration batches
forge0x May 20, 2026
075340e
feat(cutover): add migration lifecycle controls
forge0x May 20, 2026
8f8b3eb
fix(cutover): align migration indexes with run ids
forge0x May 20, 2026
50f7bb4
feat(cutover): add operator command script
forge0x May 20, 2026
98a7021
chore(cutover): format migration state helpers
forge0x May 20, 2026
e7a5965
feat(cutover): preview dry-run migration plan
forge0x May 20, 2026
751067a
fix(cutover): satisfy production build types
forge0x May 20, 2026
3fe880b
feat(cutover): add operator controls and verification
forge0x May 25, 2026
29827ed
feat(cutover): add client-aware cash wallet presentation
forge0x May 25, 2026
533967c
chore(cutover): clean operator artifact whitespace
forge0x May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,29 @@ type CashoutOffer
walletId: WalletId!
}

type CashWalletCutover
@join__type(graph: PUBLIC)
{
completedAt: Timestamp
cutoverVersion: Int!
pauseReason: String
pausedAt: Timestamp
runId: String
scheduledAt: Timestamp
startedAt: Timestamp
state: CashWalletCutoverState!
updatedAt: Timestamp!
updatedBy: String
}

enum CashWalletCutoverState
@join__type(graph: PUBLIC)
{
COMPLETE @join__enumValue(graph: PUBLIC)
IN_PROGRESS @join__enumValue(graph: PUBLIC)
PRE @join__enumValue(graph: PUBLIC)
}

"""(Positive) Cent amount (1/100 of a dollar)"""
scalar CentAmount
@join__type(graph: PUBLIC)
Expand Down Expand Up @@ -1651,6 +1674,7 @@ type Query
btcPrice(currency: DisplayCurrency! = "USD"): Price @deprecated(reason: "Deprecated in favor of realtimePrice")
btcPriceList(range: PriceGraphRange!): [PricePoint]
businessMapMarkers: [MapMarker!]!
cashWalletCutover: CashWalletCutover!
currencyList: [Currency!]!
globals: Globals
isFlashNpub(input: IsFlashNpubInput!): IsFlashNpubPayload
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
meta {
name: 01-query-admin-state
type: graphql
seq: 1
}

post {
url: {{admin_url}}
body: graphql
auth: bearer
}

auth:bearer {
token: {{admin_token}}
}

body:graphql {
query CashWalletCutoverAdminState {
cashWalletCutover {
state
scheduledAt
startedAt
completedAt
pausedAt
pauseReason
cutoverVersion
runId
updatedBy
updatedAt
}
}
}

body:graphql:vars {
{}
}

script:post-response {
test("admin cutover state returns without GraphQL errors", function () {
const jsonData = res.getBody()
expect(jsonData.errors).to.be.undefined
expect(jsonData.data.cashWalletCutover.state).to.be.oneOf([
"PRE",
"IN_PROGRESS",
"COMPLETE",
])
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
meta {
name: 02-set-scheduled-pre
type: graphql
seq: 2
}

post {
url: {{admin_url}}
body: graphql
auth: bearer
}

auth:bearer {
token: {{admin_token}}
}

body:graphql {
mutation CashWalletCutoverSetScheduledPre($input: CashWalletCutoverUpdateInput!) {
cashWalletCutoverUpdate(input: $input) {
errors {
message
}
cashWalletCutover {
state
scheduledAt
startedAt
completedAt
pauseReason
cutoverVersion
runId
updatedBy
updatedAt
}
}
}
}

body:graphql:vars {
{
"input": {
"state": "PRE",
"scheduledAt": "2026-05-22T15:00:00.000Z",
"cutoverVersion": 345,
"runId": "manual-eng-345",
"pauseReason": "manual ENG-345 preflight"
}
}
}

script:post-response {
test("sets cutover config to PRE with schedule metadata", function () {
const jsonData = res.getBody()
expect(jsonData.errors).to.be.undefined
expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([])

const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover
expect(config.state).to.eql("PRE")
expect(config.cutoverVersion).to.eql(345)
expect(config.runId).to.eql("manual-eng-345")
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
meta {
name: 03-set-in-progress
type: graphql
seq: 3
}

post {
url: {{admin_url}}
body: graphql
auth: bearer
}

auth:bearer {
token: {{admin_token}}
}

body:graphql {
mutation CashWalletCutoverSetInProgress($input: CashWalletCutoverUpdateInput!) {
cashWalletCutoverUpdate(input: $input) {
errors {
message
}
cashWalletCutover {
state
scheduledAt
startedAt
completedAt
pauseReason
cutoverVersion
runId
updatedBy
updatedAt
}
}
}
}

body:graphql:vars {
{
"input": {
"state": "IN_PROGRESS",
"cutoverVersion": 345,
"runId": "manual-eng-345",
"pauseReason": "manual ENG-345 in-progress test"
}
}
}

script:post-response {
test("sets cutover config to IN_PROGRESS", function () {
const jsonData = res.getBody()
expect(jsonData.errors).to.be.undefined
expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([])

const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover
expect(config.state).to.eql("IN_PROGRESS")
expect(config.cutoverVersion).to.eql(345)
expect(config.runId).to.eql("manual-eng-345")
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
meta {
name: 04-set-complete
type: graphql
seq: 4
}

post {
url: {{admin_url}}
body: graphql
auth: bearer
}

auth:bearer {
token: {{admin_token}}
}

body:graphql {
mutation CashWalletCutoverSetComplete($input: CashWalletCutoverUpdateInput!) {
cashWalletCutoverUpdate(input: $input) {
errors {
message
}
cashWalletCutover {
state
scheduledAt
startedAt
completedAt
pauseReason
cutoverVersion
runId
updatedBy
updatedAt
}
}
}
}

body:graphql:vars {
{
"input": {
"state": "COMPLETE",
"cutoverVersion": 345,
"runId": "manual-eng-345",
"pauseReason": "manual ENG-345 complete test"
}
}
}

script:post-response {
test("sets cutover config to COMPLETE", function () {
const jsonData = res.getBody()
expect(jsonData.errors).to.be.undefined
expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([])

const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover
expect(config.state).to.eql("COMPLETE")
expect(config.cutoverVersion).to.eql(345)
expect(config.runId).to.eql("manual-eng-345")
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
meta {
name: cash-wallet-cutover
seq: 10
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
meta {
name: cash-wallet-cutover
type: graphql
seq: 11
}

post {
url: {{flashGraphqlUrl}}
body: graphql
auth: inherit
}

body:graphql {
query CashWalletCutoverPublicState {
cashWalletCutover {
state
scheduledAt
startedAt
completedAt
pausedAt
pauseReason
cutoverVersion
runId
updatedBy
updatedAt
}
}
}

body:graphql:vars {
{}
}

script:post-response {
test("public cutover state returns without GraphQL errors", function () {
const jsonData = res.getBody()
expect(jsonData.errors).to.be.undefined
expect(jsonData.data.cashWalletCutover.state).to.be.oneOf([
"PRE",
"IN_PROGRESS",
"COMPLETE",
])
})
}
6 changes: 6 additions & 0 deletions operator-runs/eng-345-manual-347/findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Findings

- `preparePrimaryCashWalletCutover` accepts injected repositories, so the test can scope discovery to the 10 created accounts by passing an `accountsRepo.listUnlockedAccounts()` generator for only those IDs.
- Account creation creates both USD and USDT checking wallets and defaults new accounts to USDT. For this test, each new account must be explicitly updated back to the USD wallet with `Accounts.updateDefaultWalletId`.
- Funding can use `Payments.intraledgerPaymentSendWalletIdForUsdWallet` from the funder account's USD wallet to each target legacy USD wallet. The amount argument is cents, so `$0.25` is `25` and `$0.01` is `1`.
- Cutover batch execution can use `CashWalletCutover.runPrimaryCashWalletCutoverBatch`; status and lifecycle can use the existing app lifecycle functions.
Loading