aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/payments.ts4
-rw-r--r--lib/square.ts271
2 files changed, 43 insertions, 232 deletions
diff --git a/lib/payments.ts b/lib/payments.ts
index 655786fd..bd045f98 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -6,6 +6,7 @@ import ln from "$lib/ln";
import { err, l, warn } from "$lib/logging";
import { handleZap } from "$lib/nostr";
import { notify, nwcNotify } from "$lib/notifications";
+import { squarePayment } from "$lib/square";
import {
SATS,
btc,
@@ -320,6 +321,9 @@ export const completePayment = async (inv, p, user) => {
nwcNotify(p);
notify(p, user, withdrawal);
+
+ if (user.square) squarePayment(user);
+
l(username, "received", p.type, p.amount);
callWebhook(inv, p);
};
diff --git a/lib/square.ts b/lib/square.ts
index 11710774..3fbc8774 100644
--- a/lib/square.ts
+++ b/lib/square.ts
@@ -1,248 +1,55 @@
-// squareSync.ts — Square v40+, per-user OAuth creds from Redis
-import { SquareClient, SquareEnvironment, Square } from "square";
-import config from "$config";
import { g, s } from "$lib/db";
-import { l, warn } from "$lib/logging";
-
-// ----- Types coming from your code -----
-type CoinosInv = {
- id: string;
- currency: string | Square.Currency;
- rate: number; // fiat per BTC captured at invoice time
- items?: Array<any>;
- memo?: string;
-};
-
-type CoinosPayment = {
- id: string; // Coinos payment UUID
- iid: string; // invoice id
- amount: number; // sats, excludes tip
- tip: number; // sats
- rate: number; // fiat per BTC (final)
- currency: string | Square.Currency;
- type: "bitcoin" | "liquid" | "lightning" | "internal" | string;
- ref?: string; // on-chain: "...:txid:vout", LN: preimage etc.
- payment_hash?: string; // LN hash if available
- memo?: string;
-};
-
-type CoinosUser = { id: string; username?: string };
-
-// ----- Helpers -----
-const toBigCents = (n: number) => BigInt(n);
-const satsToCents = (sats: number, fiatPerBTC: number) =>
- Math.round((sats / 100_000_000) * fiatPerBTC * 100);
-
-const nowISO = () => new Date().toISOString();
-const isPast = (iso?: string, skewSeconds = 60) =>
- !!iso && Date.parse(iso) - skewSeconds * 1000 <= Date.now();
-
-const normalizeCurrency = (c: string | Square.Currency): Square.Currency =>
- (typeof c === "string" ? c : String(c)).toUpperCase() as Square.Currency;
-
-const networkLabel = (t: CoinosPayment["type"]) =>
- t === "lightning" ? "LIGHTNING" : t === "bitcoin" || t === "liquid" ? "ONCHAIN" : "OTHER";
-
-// ----- Per-user Square auth in Redis -----
-// Key: `${uid}:square` -> JSON: { accessToken, tokenType, expiresAt, merchantId, refreshToken, shortLived, refreshTokenExpiresAt }
-type RedisSquareAuth = {
- accessToken: string;
- tokenType: "bearer";
- expiresAt?: string; // ISO
- merchantId?: string;
- refreshToken?: string;
- shortLived?: boolean;
- refreshTokenExpiresAt?: string;
-};
+import config from "$config";
+import { SquareClient } from "square";
+import { v4 } from "uuid";
-async function loadSquareAuth(uid: string): Promise<RedisSquareAuth | null> {
- const raw = await g(`${uid}:square`);
- if (!raw) return null;
- return typeof raw === "string" ? JSON.parse(raw) : (raw as RedisSquareAuth);
-}
+const { appId, environment } = config.square;
-// Save back (preserve unknown fields)
-async function saveSquareAuth(uid: string, next: Partial<RedisSquareAuth>) {
- const current = (await loadSquareAuth(uid)) || ({} as RedisSquareAuth);
- const merged = { ...current, ...next };
- await s(`${uid}:square`, JSON.stringify(merged));
- return merged;
-}
+export const squarePayment = async (user) => {
+ let square = await g(`${user.id}:square`);
-// Create an SDK client for any token (or no token for OAuth)
-function makeClient(token?: string) {
- return new SquareClient({
- token: token ?? "", // OAuth obtainToken doesn’t require bearer auth
- environment:
- config.env === "production"
- ? SquareEnvironment.Production
- : SquareEnvironment.Sandbox,
+ let client = new SquareClient({
+ environment,
});
-}
-
-// Refresh the user’s access token if needed
-async function ensureFreshAccessToken(uid: string): Promise<RedisSquareAuth | null> {
- const auth = await loadSquareAuth(uid);
- if (!auth) return null;
- // If no expiry recorded, just use the token as-is
- if (!auth.expiresAt || !isPast(auth.expiresAt)) return auth;
+ const { refreshToken } = square;
- // Expired — try to refresh
- if (!auth.refreshToken) {
- warn("Square: token expired and no refresh token present", uid);
- return auth;
- }
- if (!config.square?.clientId || !config.square?.clientSecret) {
- warn("Square: missing clientId/clientSecret for OAuth refresh");
- return auth;
- }
-
- try {
- const oauth = makeClient().oAuth; // new SDK OAuth resource
- const res = await oauth.obtainToken({
- clientId: config.square.clientId,
- clientSecret: config.square.clientSecret,
- grantType: "refresh_token",
- refreshToken: auth.refreshToken,
- });
- // Update stored creds (Square may rotate fields)
- const updated = await saveSquareAuth(uid, {
- accessToken: res.accessToken!,
- tokenType: (res.tokenType as "bearer") ?? "bearer",
- expiresAt: res.expiresAt,
- merchantId: res.merchantId ?? auth.merchantId,
- refreshToken: res.refreshToken ?? auth.refreshToken,
- // shortLived is optional; keep existing if not provided
- shortLived: typeof (res as any).shortLived === "boolean" ? (res as any).shortLived : auth.shortLived,
- refreshTokenExpiresAt: (res as any).refreshTokenExpiresAt ?? auth.refreshTokenExpiresAt,
- });
- l("Square OAuth refresh ok", { uid, oldExp: auth.expiresAt, newExp: updated.expiresAt });
- return updated;
- } catch (e: any) {
- warn("Square OAuth refresh failed", e?.message || e);
- return auth; // fall back to (expired) auth — caller can decide to skip
- }
-}
-
-// Pick/cached location for the seller
-async function getOrCacheLocationId(uid: string, client: SquareClient): Promise<string | null> {
- const cacheKey = `${uid}:square:locationId`;
- const fromCache = await g(cacheKey);
- if (typeof fromCache === "string" && fromCache) return fromCache;
- if (fromCache && (fromCache as any).id) return (fromCache as any).id;
-
- // Query seller’s locations and choose the first ACTIVE (or first if none ACTIVE)
- const list = await client.locations.list();
- // Auto-pagination can return a pager; handle both shapes
- let firstActiveId: string | undefined;
- if (Array.isArray((list as any).data)) {
- const page = (list as any).data as Square.Location[];
- firstActiveId = page.find((loc) => loc.status === "ACTIVE")?.id || page[0]?.id;
- } else {
- const resp = list as { locations?: Square.Location[] };
- firstActiveId = resp.locations?.find((loc) => loc.status === "ACTIVE")?.id || resp.locations?.[0]?.id;
- }
-
- if (!firstActiveId) {
- warn("Square: no locations found for seller", uid);
- return null;
- }
-
- await s(cacheKey, firstActiveId);
- return firstActiveId;
-}
-
-export async function syncSquare(inv: CoinosInv, p: CoinosPayment, user: CoinosUser) {
- // 1) Load/refresh per-user OAuth creds
- const auth = await ensureFreshAccessToken(user.id);
- if (!auth?.accessToken) {
- warn("Square: no credentials for user; skipping sync", user.id);
- return;
- }
+ square = await client.oAuth.obtainToken({
+ clientId: appId,
+ grantType: "refresh_token",
+ refreshToken,
+ });
- // 2) Build a client for this seller and find a location
- const client = makeClient(auth.accessToken);
- const locationId = await getOrCacheLocationId(user.id, client);
- if (!locationId) {
- warn("Square: no locationId; skipping sync");
- return;
- }
+ await s(`${user.id}:square`, square);
- // 3) Amounts and currency
- const itemSats = p.amount || 0;
- const tipSats = p.tip || 0;
- const itemCents = satsToCents(itemSats, p.rate);
- const tipCents = satsToCents(tipSats, p.rate);
- const totalCents = itemCents + tipCents;
- const cur = normalizeCurrency(p.currency || inv.currency);
+ const { accessToken } = square;
- // 4) Create (or reuse) an Order
- const orderResp = await client.orders.create({
- idempotencyKey: `coinos-order-${inv.id}`,
- order: {
- locationId,
- metadata: {
- coinos_invoice_id: inv.id,
- coinos_payment_id: p.id,
- coinos_user_id: user.id,
- coinos_synced_at: nowISO(),
- },
- lineItems: [
- {
- name: inv.memo?.slice(0, 255) || "Coinos Order",
- quantity: "1",
- basePriceMoney: { amount: toBigCents(itemCents), currency: cur },
- },
- ],
- ...(tipCents > 0
- ? {
- serviceCharges: [
- {
- name: "Tip",
- calculationPhase: "TOTAL_PHASE",
- taxable: false,
- amountMoney: { amount: toBigCents(tipCents), currency: cur },
- },
- ],
- }
- : {}),
- },
+ client = new SquareClient({
+ token: accessToken,
+ environment,
});
- const orderId = orderResp.order?.id;
- if (!orderId) throw new Error("Square: create order returned no order.id");
-
- // 5) Record an EXTERNAL payment (approved, not autocompleted)
- const short = (s?: string, n = 18) => (s ? (s.length > n ? s.slice(0, n) + "…" : s) : "");
- const refParts = (p.ref || "").split(":");
- const maybeTxid = refParts.length >= 2 ? refParts[refParts.length - 2] : undefined;
- const note = [
- `Coinos invoice ${inv.id}`,
- p.payment_hash ? `hash ${short(p.payment_hash)}` : "",
- maybeTxid ? `txid ${short(maybeTxid)}` : "",
- inv.memo ? `memo ${short(inv.memo, 60)}` : "",
- ].filter(Boolean).join(" | ");
+ // const payments = await authedClient.payments.list({});
+ // console.log(payments);
+ const locs = await client.locations.list({});
- const payResp = await client.payments.create({
- idempotencyKey: `coinos-pay-${p.id}`,
- locationId,
- orderId,
+ const pay = await client.payments.create({
sourceId: "EXTERNAL",
- amountMoney: { amount: toBigCents(totalCents), currency: cur },
- autocomplete: false, // keep APPROVED; we’ll pay the order next
- externalDetails: { type: "OTHER", source: `Coinos ${networkLabel(p.type)}` },
- note,
- });
- const paymentId = payResp.payment?.id;
- if (!paymentId) throw new Error("Square: create payment returned no payment.id");
-
- // 6) Pay the order with our external payment
- await client.orders.pay({
- orderId,
- idempotencyKey: `coinos-payorder-${orderId}-${paymentId}`,
- paymentIds: [paymentId],
+ idempotencyKey: v4(),
+ amountMoney: {
+ amount: BigInt(1000),
+ currency: "CAD",
+ },
+ // appFeeMoney: {
+ // amount: BigInt(10),
+ // currency: "CAD",
+ // },
+ autocomplete: true,
+ externalDetails: { type: "OTHER", source: `Coinos ${v4()}` },
+ locationId: locs.locations[0].id,
+ referenceId: v4(),
+ note: `Brief description ${v4()}`,
});
- l("Square sync complete", { orderId, paymentId, invoice: inv.id, user: user.id, locationId });
-}
+ console.log(pay);
+};