ensure correct env
This commit is contained in:
parent
2280748651
commit
c3651b1695
3 changed files with 53 additions and 65 deletions
7
.github/workflows/stripe-to-invoice.yml
vendored
7
.github/workflows/stripe-to-invoice.yml
vendored
|
|
@ -128,6 +128,7 @@ jobs:
|
||||||
XERO_CLIENT_ID="$PROD_XERO_CLIENT_ID"
|
XERO_CLIENT_ID="$PROD_XERO_CLIENT_ID"
|
||||||
XERO_CLIENT_SECRET="$PROD_XERO_SECRET_KEY"
|
XERO_CLIENT_SECRET="$PROD_XERO_SECRET_KEY"
|
||||||
XERO_REDIRECT_URI="$PROD_REDIRECT_URI"
|
XERO_REDIRECT_URI="$PROD_REDIRECT_URI"
|
||||||
|
AWS_REGION="$DEV_AWS_REGION"
|
||||||
|
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
@ -138,6 +139,7 @@ jobs:
|
||||||
XERO_CLIENT_ID="$DEV_XERO_CLIENT_ID"
|
XERO_CLIENT_ID="$DEV_XERO_CLIENT_ID"
|
||||||
XERO_CLIENT_SECRET="$DEV_XERO_SECRET_KEY"
|
XERO_CLIENT_SECRET="$DEV_XERO_SECRET_KEY"
|
||||||
XERO_REDIRECT_URI="$DEV_XERO_REDIRECT_URI"
|
XERO_REDIRECT_URI="$DEV_XERO_REDIRECT_URI"
|
||||||
|
AWS_REGION="$PROD_AWS_REGION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
: "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}"
|
: "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}"
|
||||||
|
|
@ -147,6 +149,7 @@ jobs:
|
||||||
: "${XERO_CLIENT_ID:?missing XERO_CLIENT_ID}"
|
: "${XERO_CLIENT_ID:?missing XERO_CLIENT_ID}"
|
||||||
: "${XERO_CLIENT_SECRET:?missing XERO_CLIENT_SECRET}"
|
: "${XERO_CLIENT_SECRET:?missing XERO_CLIENT_SECRET}"
|
||||||
: "${XERO_REDIRECT_URI:?missing XERO_REDIRECT_URI}"
|
: "${XERO_REDIRECT_URI:?missing XERO_REDIRECT_URI}"
|
||||||
|
: "${AWS_REGION:?missing AWS_REGION}"
|
||||||
|
|
||||||
export \
|
export \
|
||||||
STRIPE_SECRET_KEY \
|
STRIPE_SECRET_KEY \
|
||||||
|
|
@ -156,7 +159,9 @@ jobs:
|
||||||
XERO_CLIENT_ID \
|
XERO_CLIENT_ID \
|
||||||
XERO_CLIENT_SECRET \
|
XERO_CLIENT_SECRET \
|
||||||
XERO_REDIRECT_URI \
|
XERO_REDIRECT_URI \
|
||||||
NAMESPACE
|
AWS_REGION \
|
||||||
|
NAMESPACE
|
||||||
|
|
||||||
|
|
||||||
envsubst < stripe_to_invoice/deployment/secrets/stripe-secrets.yaml \
|
envsubst < stripe_to_invoice/deployment/secrets/stripe-secrets.yaml \
|
||||||
| kubectl apply -f -
|
| kubectl apply -f -
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,20 @@ export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getStripe } from "@/lib/stripe/service";
|
|
||||||
import { XeroClient, Invoice, CurrencyCode } from "xero-node";
|
|
||||||
import Stripe from "stripe";
|
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 { db } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
stripeAccounts,
|
stripeAccounts,
|
||||||
xeroConnections,
|
xeroConnections,
|
||||||
processedStripeEvents,
|
processedStripeEvents,
|
||||||
} from "@/lib/schema";
|
} from "@/lib/schema";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { getXeroClient } from "@/lib/xero/service";
|
|
||||||
|
|
||||||
|
|
||||||
const stripe = getStripe();
|
const stripe = getStripe();
|
||||||
|
|
||||||
|
|
@ -98,7 +100,6 @@ export async function POST(req: NextRequest) {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!xeroConn) {
|
if (!xeroConn) {
|
||||||
console.error("❌ No Xero connection for user:", stripeAccount.userId);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "User has no Xero connection" },
|
{ error: "User has no Xero connection" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|
@ -107,39 +108,15 @@ export async function POST(req: NextRequest) {
|
||||||
|
|
||||||
if (!xeroConn.salesAccountCode || !xeroConn.stripeClearingAccountCode) {
|
if (!xeroConn.salesAccountCode || !xeroConn.stripeClearingAccountCode) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Xero account codes not configured. User must select Sales and Stripe Clearing accounts."
|
"Xero account codes not configured (sales / stripe clearing)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 5️⃣ Init Xero client + refresh token if needed
|
// 5️⃣ Get VALID Xero access token (refresh handled centrally)
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
const xero = getXeroClient();
|
const accessToken = await getValidXeroAccessToken(stripeAccount.userId);
|
||||||
|
const xero = getXeroClient(accessToken);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 6️⃣ Resolve contact (email-only)
|
// 6️⃣ Resolve contact (email-only)
|
||||||
|
|
@ -168,9 +145,7 @@ export async function POST(req: NextRequest) {
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
const created = await xero.accountingApi.createContacts(
|
const created = await xero.accountingApi.createContacts(
|
||||||
xeroConn.tenantId,
|
xeroConn.tenantId,
|
||||||
{
|
{ contacts: [{ name, emailAddress: email }] }
|
||||||
contacts: [{ name, emailAddress: email }],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
contact = created.body.contacts?.[0];
|
contact = created.body.contacts?.[0];
|
||||||
}
|
}
|
||||||
|
|
@ -182,15 +157,14 @@ export async function POST(req: NextRequest) {
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 7️⃣ Create AUTHORISED invoice
|
// 7️⃣ Create AUTHORISED invoice
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
const amount = session.amount_total! / 100;
|
if (!session.amount_total || !session.currency) {
|
||||||
|
throw new Error("Stripe session missing amount or currency");
|
||||||
if (!session.currency) {
|
|
||||||
throw new Error("Stripe session missing 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}`);
|
throw new Error(`Unsupported currency: ${session.currency}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,7 +184,7 @@ export async function POST(req: NextRequest) {
|
||||||
accountCode: xeroConn.salesAccountCode,
|
accountCode: xeroConn.salesAccountCode,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
currencyCode: CurrencyCode[currencyCode],
|
currencyCode: CurrencyCode[currencyKey],
|
||||||
reference: session.id,
|
reference: session.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -223,20 +197,13 @@ export async function POST(req: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 8️⃣ Mark invoice as PAID to Stripe Clearing
|
// 8️⃣ Mark invoice as PAID → 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
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
const paymentReference =
|
const paymentReference =
|
||||||
typeof session.payment_intent === "string"
|
typeof session.payment_intent === "string"
|
||||||
? session.payment_intent
|
? session.payment_intent
|
||||||
: session.id;
|
: session.id;
|
||||||
|
|
||||||
await xero.accountingApi.createPayments(xeroConn.tenantId, {
|
await xero.accountingApi.createPayments(xeroConn.tenantId, {
|
||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
|
|
@ -244,22 +211,20 @@ export async function POST(req: NextRequest) {
|
||||||
amount,
|
amount,
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: new Date().toISOString().slice(0, 10),
|
||||||
reference: paymentReference,
|
reference: paymentReference,
|
||||||
account: {
|
account: { code: xeroConn.stripeClearingAccountCode },
|
||||||
code: xeroConn.stripeClearingAccountCode,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 9️⃣ Record idempotency AFTER success
|
// 9️⃣ Record idempotency (LAST STEP)
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
await db.insert(processedStripeEvents).values({
|
await db.insert(processedStripeEvents).values({
|
||||||
stripeEventId: event.id,
|
stripeEventId: event.id,
|
||||||
stripeAccountId,
|
stripeAccountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Stripe payment fully processed", {
|
console.log("✅ Stripe → Xero sync complete", {
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
invoiceId: invoice.invoiceID,
|
invoiceId: invoice.invoiceID,
|
||||||
stripeAccountId,
|
stripeAccountId,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,23 @@
|
||||||
import { XeroClient } from "xero-node";
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue