diff options
| author | Adam Soltys <[email protected]> | 2025-09-06 16:46:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2025-09-06 16:46:39 -0700 |
| commit | 66ea0286057f43c699f4701cb4e444ff2308a414 (patch) | |
| tree | 2b712a8e12b9ce7c21baab1f63b21041c45eddc8 /lib/square.ts | |
| parent | 780a7cca58f3e34140fd9430e2ae02145e4149e0 (diff) | |
| download | coinos-server-66ea0286057f43c699f4701cb4e444ff2308a414.tar.gz coinos-server-66ea0286057f43c699f4701cb4e444ff2308a414.zip | |
wip
Diffstat (limited to 'lib/square.ts')
| -rw-r--r-- | lib/square.ts | 271 |
1 files changed, 39 insertions, 232 deletions
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); +}; |
