From 613be810551f44e79be84d5e5a944a8593db7166 Mon Sep 17 00:00:00 2001 From: olaven Date: Tue, 20 May 2025 21:49:17 +0200 Subject: [PATCH] FIX: existing agreement exists, but is out of sync --- .gitignore | 1 + lib/vipps/vipps.ts | 82 ++++++++++++++++------------ routes/api/trigger-donation/vipps.ts | 20 ++++++- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index a49d6cb..9c1cbf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +test-script.ts tmp # Created by https://www.toptal.com/developers/gitignore/api/node,deno,macos,emacs diff --git a/lib/vipps/vipps.ts b/lib/vipps/vipps.ts index 33cdeb3..a11abf4 100644 --- a/lib/vipps/vipps.ts +++ b/lib/vipps/vipps.ts @@ -7,8 +7,12 @@ import { STATUS_CODE } from "$fresh/server.ts"; const config = { clientId: Deno.env.get("VIPPS_CLIENT_ID"), clientSecret: Deno.env.get("VIPPS_CLIENT_SECRET"), - ocpApimSubscriptionKeyPrimary: Deno.env.get("VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY"), - ocpApimSubscriptionKeySecondary: Deno.env.get("VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY"), + ocpApimSubscriptionKeyPrimary: Deno.env.get( + "VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY", + ), + ocpApimSubscriptionKeySecondary: Deno.env.get( + "VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY", + ), msn: Deno.env.get("VIPPS_MSN"), baseUrl: Deno.env.get("VIPPS_API_BASE_URL"), }; @@ -33,8 +37,8 @@ const getAccessToken = async function () { method: "POST", // @ts-ignore headers: { - "client_id": config.clientId, - "client_secret": config.clientSecret, + client_id: config.clientId, + client_secret: config.clientSecret, ...standardVippsHeaders, }, }); @@ -56,23 +60,23 @@ export const createAgreement = async function (email: string) { ...standardVippsHeaders, }, body: JSON.stringify({ - "interval": { - "unit": "MONTH", - "count": 1, + interval: { + unit: "MONTH", + count: 1, }, - "initialCharge": { - "amount": amount, - "description": "Initial charge", - "transactionType": "DIRECT_CAPTURE", + initialCharge: { + amount: amount, + description: "Initial charge", + transactionType: "DIRECT_CAPTURE", }, - "pricing": { - "amount": amount, - "currency": "NOK", + pricing: { + amount: amount, + currency: "NOK", }, // email is used to identify the user in the success page - "merchantRedirectUrl": `${getHostUrl()}/donations-success?email=${email}`, - "merchantAgreementUrl": `${getHostUrl()}`, - "productName": "Månedlig støtte til NRSS", + merchantRedirectUrl: `${getHostUrl()}/donations-success?email=${email}`, + merchantAgreementUrl: `${getHostUrl()}`, + productName: "Månedlig støtte til NRSS", }), }); @@ -89,15 +93,20 @@ export const createAgreement = async function (email: string) { } }; -export const getAgreement = async function (agreementId: string) { +export const getAgreement = async function (agreementId: string): Promise<{ + status: "PENDING" | "ACTIVE" | "STOPPED" | "EXPIRED"; +}> { const token = await getAccessToken(); - const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, { - // @ts-ignore - headers: { - authorization: `Bearer ${token}`, - ...standardVippsHeaders, + const response = await fetch( + `${config.baseUrl}/recurring/v3/agreements/${agreementId}`, + { + // @ts-ignore + headers: { + authorization: `Bearer ${token}`, + ...standardVippsHeaders, + }, }, - }); + ); if (response.status === STATUS_CODE.OK) { return response.json(); @@ -110,18 +119,21 @@ export const getAgreement = async function (agreementId: string) { export const cancelAgreement = async function (agreementId: string) { const token = await getAccessToken(); - const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, { - method: "PATCH", - // @ts-ignore - headers: { - authorization: `Bearer ${token}`, - "Idempotency-Key": `${agreementId}-${Date.now()}`, - ...standardVippsHeaders, + const response = await fetch( + `${config.baseUrl}/recurring/v3/agreements/${agreementId}`, + { + method: "PATCH", + // @ts-ignore + headers: { + authorization: `Bearer ${token}`, + "Idempotency-Key": `${agreementId}-${Date.now()}`, + ...standardVippsHeaders, + }, + body: JSON.stringify({ + status: "STOPPED", + }), }, - body: JSON.stringify({ - "status": "STOPPED", - }), - }); + ); if (response.status === STATUS_CODE.NoContent) { return true; diff --git a/routes/api/trigger-donation/vipps.ts b/routes/api/trigger-donation/vipps.ts index 7d20900..4f67fd6 100644 --- a/routes/api/trigger-donation/vipps.ts +++ b/routes/api/trigger-donation/vipps.ts @@ -1,6 +1,7 @@ import { getHostUrl, validateEmail } from "../../../lib/utils.ts"; import { storage } from "../../../lib/storage.ts"; import { createAgreement } from "../../../lib/vipps/vipps.ts"; +import { getAgreement } from "../../../lib/vipps/vipps.ts"; export const handler = async function (req: Request): Promise { const email = new URLSearchParams(req.url.split("?")[1] || "").get("email"); @@ -21,11 +22,26 @@ export const handler = async function (req: Request): Promise { const hasActiveAgreement = existingAgreement && existingAgreement.validAt !== null && existingAgreement.revokedAt === null; + + const agreementInVipps = existingAgreement && (await getAgreement(existingAgreement.agreementId)); + if ( + agreementInVipps && + agreementInVipps.status !== "ACTIVE" && hasActiveAgreement ) { - console.error("Agreement already exists", email); - return Response.redirect(errorPage); + // Somehow the Vipps agreement is not active in our system anymore. + // We should reflect the new status in our system. + await storage.writeVippsAgreement({ + ...existingAgreement, + revokedAt: new Date(), + }); + } else { + // i.e. the agreement in our system was in sync with Vipps. + if (hasActiveAgreement) { + console.error("Agreement already exists", email); + return Response.redirect(errorPage); + } } const agreement = await createAgreement(email);