ensure correct env

This commit is contained in:
Jun-te Kim 2026-01-21 00:02:04 +00:00
parent 2280748651
commit c3651b1695
3 changed files with 53 additions and 65 deletions

View file

@ -128,6 +128,7 @@ jobs:
XERO_CLIENT_ID="$PROD_XERO_CLIENT_ID"
XERO_CLIENT_SECRET="$PROD_XERO_SECRET_KEY"
XERO_REDIRECT_URI="$PROD_REDIRECT_URI"
AWS_REGION="$DEV_AWS_REGION"
else
@ -138,6 +139,7 @@ jobs:
XERO_CLIENT_ID="$DEV_XERO_CLIENT_ID"
XERO_CLIENT_SECRET="$DEV_XERO_SECRET_KEY"
XERO_REDIRECT_URI="$DEV_XERO_REDIRECT_URI"
AWS_REGION="$PROD_AWS_REGION"
fi
: "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}"
@ -147,6 +149,7 @@ jobs:
: "${XERO_CLIENT_ID:?missing XERO_CLIENT_ID}"
: "${XERO_CLIENT_SECRET:?missing XERO_CLIENT_SECRET}"
: "${XERO_REDIRECT_URI:?missing XERO_REDIRECT_URI}"
: "${AWS_REGION:?missing AWS_REGION}"
export \
STRIPE_SECRET_KEY \
@ -156,7 +159,9 @@ jobs:
XERO_CLIENT_ID \
XERO_CLIENT_SECRET \
XERO_REDIRECT_URI \
NAMESPACE
AWS_REGION \
NAMESPACE
envsubst < stripe_to_invoice/deployment/secrets/stripe-secrets.yaml \
| kubectl apply -f -

View file

@ -3,18 +3,20 @@ export const dynamic = "force-dynamic";
export const revalidate = 0;
import { NextRequest, NextResponse } from "next/server";
import { getStripe } from "@/lib/stripe/service";
import { XeroClient, Invoice, CurrencyCode } from "xero-node";
import Stripe from "stripe";
import { Invoice, CurrencyCode } from "xero-node";
import { eq } from "drizzle-orm";
import { getStripe } from "@/lib/stripe/service";
import { getXeroClient } from "@/lib/xero/service";
import { getValidXeroAccessToken } from "@/lib/xero/auth";
import { db } from "@/lib/db";
import {
stripeAccounts,
xeroConnections,
processedStripeEvents,
} from "@/lib/schema";
import { eq } from "drizzle-orm";
import { getXeroClient } from "@/lib/xero/service";
const stripe = getStripe();
@ -98,7 +100,6 @@ export async function POST(req: NextRequest) {
.limit(1);
if (!xeroConn) {
console.error("❌ No Xero connection for user:", stripeAccount.userId);
return NextResponse.json(
{ error: "User has no Xero connection" },
{ status: 500 }
@ -107,39 +108,15 @@ export async function POST(req: NextRequest) {
if (!xeroConn.salesAccountCode || !xeroConn.stripeClearingAccountCode) {
throw new Error(
"Xero account codes not configured. User must select Sales and Stripe Clearing accounts."
"Xero account codes not configured (sales / stripe clearing)"
);
}
// --------------------------------------------------
// 5Init Xero client + refresh token if needed
// 5Get VALID Xero access token (refresh handled centrally)
// --------------------------------------------------
const xero = getXeroClient();
xero.setTokenSet({
access_token: xeroConn.accessToken,
refresh_token: xeroConn.refreshToken,
expires_at: xeroConn.expiresAt.getTime(),
token_type: "Bearer",
});
const now = Date.now();
if (xeroConn.expiresAt.getTime() <= now + 60_000) {
console.log("🔄 Refreshing Xero token");
const newTokenSet = await xero.refreshToken();
await db
.update(xeroConnections)
.set({
accessToken: newTokenSet.access_token!,
refreshToken: newTokenSet.refresh_token!,
expiresAt: new Date(newTokenSet.expires_at!),
})
.where(eq(xeroConnections.id, xeroConn.id));
xero.setTokenSet(newTokenSet);
}
const accessToken = await getValidXeroAccessToken(stripeAccount.userId);
const xero = getXeroClient(accessToken);
// --------------------------------------------------
// 6⃣ Resolve contact (email-only)
@ -168,9 +145,7 @@ export async function POST(req: NextRequest) {
if (!contact) {
const created = await xero.accountingApi.createContacts(
xeroConn.tenantId,
{
contacts: [{ name, emailAddress: email }],
}
{ contacts: [{ name, emailAddress: email }] }
);
contact = created.body.contacts?.[0];
}
@ -182,15 +157,14 @@ export async function POST(req: NextRequest) {
// --------------------------------------------------
// 7⃣ Create AUTHORISED invoice
// --------------------------------------------------
const amount = session.amount_total! / 100;
if (!session.currency) {
throw new Error("Stripe session missing currency");
if (!session.amount_total || !session.currency) {
throw new Error("Stripe session missing amount or currency");
}
const currencyCode = session.currency.toUpperCase() as keyof typeof CurrencyCode;
const amount = session.amount_total / 100;
const currencyKey = session.currency.toUpperCase() as keyof typeof CurrencyCode;
if (!(currencyCode in CurrencyCode)) {
if (!(currencyKey in CurrencyCode)) {
throw new Error(`Unsupported currency: ${session.currency}`);
}
@ -210,7 +184,7 @@ export async function POST(req: NextRequest) {
accountCode: xeroConn.salesAccountCode,
},
],
currencyCode: CurrencyCode[currencyCode],
currencyCode: CurrencyCode[currencyKey],
reference: session.id,
},
],
@ -223,20 +197,13 @@ export async function POST(req: NextRequest) {
}
// --------------------------------------------------
// 8⃣ Mark invoice as PAID to Stripe Clearing
//
// STRIPE CLEARING (v1 behaviour)
// - Invoices are marked as PAID to a Stripe Clearing account
// - Stripe fees are NOT yet recorded in v1
//
// TODO (planned):
// - Record Stripe fees as an expense
// - Reconcile Stripe payouts automatically
// 8⃣ Mark invoice as PAID → Stripe Clearing
// --------------------------------------------------
const paymentReference =
typeof session.payment_intent === "string"
? session.payment_intent
: session.id;
typeof session.payment_intent === "string"
? session.payment_intent
: session.id;
await xero.accountingApi.createPayments(xeroConn.tenantId, {
payments: [
{
@ -244,22 +211,20 @@ export async function POST(req: NextRequest) {
amount,
date: new Date().toISOString().slice(0, 10),
reference: paymentReference,
account: {
code: xeroConn.stripeClearingAccountCode,
},
account: { code: xeroConn.stripeClearingAccountCode },
},
],
});
// --------------------------------------------------
// 9⃣ Record idempotency AFTER success
// 9⃣ Record idempotency (LAST STEP)
// --------------------------------------------------
await db.insert(processedStripeEvents).values({
stripeEventId: event.id,
stripeAccountId,
});
console.log("✅ Stripe payment fully processed", {
console.log("✅ Stripe → Xero sync complete", {
eventId: event.id,
invoiceId: invoice.invoiceID,
stripeAccountId,

View file

@ -1,5 +1,23 @@
import { XeroClient } from "xero-node";
export function getXeroClient(): XeroClient {
return new XeroClient();
}
/**
* Creates a XeroClient using an already-valid access token.
*
* IMPORTANT:
* - Token refresh is handled elsewhere (auth.ts)
* - This client MUST NOT call refreshToken()
*/
export function getXeroClient(accessToken: string): XeroClient {
if (!accessToken) {
throw new Error("Xero access token missing");
}
const xero = new XeroClient();
xero.setTokenSet({
access_token: accessToken,
token_type: "Bearer",
});
return xero;
}