Compare commits
3 Commits
main
...
add-anothe
| Author | SHA1 | Date |
|---|---|---|
|
|
43477f0798 | |
|
|
2109ae438f | |
|
|
0ce01b97f1 |
|
|
@ -1,3 +1,4 @@
|
||||||
|
test-script.ts
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/node,deno,macos,emacs
|
# Created by https://www.toptal.com/developers/gitignore/api/node,deno,macos,emacs
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { assertEquals, assertExists } from "$std/assert/mod.ts";
|
import { assertEquals, assertExists, assertLess } from "$std/assert/mod.ts";
|
||||||
import { testUtils } from "./test-utils.ts";
|
import { testUtils } from "./test-utils.ts";
|
||||||
import { rss } from "./rss.ts";
|
import { rss } from "./rss.ts";
|
||||||
import { forTestingOnly } from "./rss.ts";
|
import { forTestingOnly } from "./rss.ts";
|
||||||
|
|
@ -30,6 +30,20 @@ Deno.test("generated rss contains promo with link to donations page", () => {
|
||||||
const series = testUtils.generateSeries();
|
const series = testUtils.generateSeries();
|
||||||
const feed = rss.assembleFeed(series);
|
const feed = rss.assembleFeed(series);
|
||||||
|
|
||||||
console.log(feed);
|
|
||||||
feed.includes("Vurder å støtte utviklingen via Vipps");
|
feed.includes("Vurder å støtte utviklingen via Vipps");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("generated rss contains promo with link to donations page", () => {
|
||||||
|
const series = testUtils.generateSeries();
|
||||||
|
const feed = rss.assembleFeed(series);
|
||||||
|
|
||||||
|
const indexOfFirstPromotion = feed.indexOf(
|
||||||
|
"NRSS er avhengig av din Vipps-støtte",
|
||||||
|
);
|
||||||
|
const indexOfSecondPromotion = feed.indexOf("Vurder å støtte utviklingen");
|
||||||
|
assertLess(
|
||||||
|
indexOfFirstPromotion,
|
||||||
|
indexOfSecondPromotion,
|
||||||
|
"First promotion should come before the second",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
19
lib/rss.ts
19
lib/rss.ts
|
|
@ -27,16 +27,11 @@ function assembleFeed(series: Series): string {
|
||||||
tag("itunes:name", "NRK"),
|
tag("itunes:name", "NRK"),
|
||||||
tag("itunes:email", "nrkpodcast@nrk.no"),
|
tag("itunes:email", "nrkpodcast@nrk.no"),
|
||||||
]),
|
]),
|
||||||
tag(
|
tag("description", series.subtitle || ""),
|
||||||
"description",
|
|
||||||
series.subtitle || "",
|
|
||||||
),
|
|
||||||
tag("ttl", "60"), //60 minutes
|
tag("ttl", "60"), //60 minutes
|
||||||
...(series.imageUrl
|
...(series.imageUrl
|
||||||
? [
|
? [
|
||||||
tag("itunes:image", "", [
|
tag("itunes:image", "", [["href", series.imageUrl]]),
|
||||||
["href", series.imageUrl],
|
|
||||||
]),
|
|
||||||
tag("image", [
|
tag("image", [
|
||||||
tag("url", series.imageUrl),
|
tag("url", series.imageUrl),
|
||||||
tag("title", series.title),
|
tag("title", series.title),
|
||||||
|
|
@ -58,10 +53,11 @@ function assembleFeed(series: Series): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function descriptionWithDonationPromotion(description: string): string {
|
function descriptionWithDonationPromotion(description: string): string {
|
||||||
const promotion =
|
const firstPromotion = `⬇️NRSS er avhengig av din Vipps-støtte⬇️`;
|
||||||
|
const secondPromotion =
|
||||||
`Takk for at du bruker NRSS 🙏🌟 Vurder å støtte utviklingen via Vipps med omtrent det samme som prisen på en kaffekopp. Se mer på https://nrss.deno.dev/`;
|
`Takk for at du bruker NRSS 🙏🌟 Vurder å støtte utviklingen via Vipps med omtrent det samme som prisen på en kaffekopp. Se mer på https://nrss.deno.dev/`;
|
||||||
|
|
||||||
return `${description}\n\n${promotion}`;
|
return `${firstPromotion}\n\n${description}\n\n${secondPromotion}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assembleEpisode(episode: Episode, seriesId: Series["id"]): Tag {
|
function assembleEpisode(episode: Episode, seriesId: Series["id"]): Tag {
|
||||||
|
|
@ -76,10 +72,7 @@ function assembleEpisode(episode: Episode, seriesId: Series["id"]): Tag {
|
||||||
tag("pubDate", new Date(episode.date).toUTCString()),
|
tag("pubDate", new Date(episode.date).toUTCString()),
|
||||||
tag("itunes:duration", episode.durationInSeconds.toString()),
|
tag("itunes:duration", episode.durationInSeconds.toString()),
|
||||||
tag("podcast:chapters", "", [
|
tag("podcast:chapters", "", [
|
||||||
[
|
["url", `${getHostUrl()}/api/feeds/${seriesId}/${episode.id}/chapters`],
|
||||||
"url",
|
|
||||||
`${getHostUrl()}/api/feeds/${seriesId}/${episode.id}/chapters`,
|
|
||||||
],
|
|
||||||
["type", "application/json+chapters"],
|
["type", "application/json+chapters"],
|
||||||
]),
|
]),
|
||||||
tag("enclosure", "", [
|
tag("enclosure", "", [
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ import { STATUS_CODE } from "$fresh/server.ts";
|
||||||
const config = {
|
const config = {
|
||||||
clientId: Deno.env.get("VIPPS_CLIENT_ID"),
|
clientId: Deno.env.get("VIPPS_CLIENT_ID"),
|
||||||
clientSecret: Deno.env.get("VIPPS_CLIENT_SECRET"),
|
clientSecret: Deno.env.get("VIPPS_CLIENT_SECRET"),
|
||||||
ocpApimSubscriptionKeyPrimary: Deno.env.get("VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY"),
|
ocpApimSubscriptionKeyPrimary: Deno.env.get(
|
||||||
ocpApimSubscriptionKeySecondary: Deno.env.get("VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY"),
|
"VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY",
|
||||||
|
),
|
||||||
|
ocpApimSubscriptionKeySecondary: Deno.env.get(
|
||||||
|
"VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY",
|
||||||
|
),
|
||||||
msn: Deno.env.get("VIPPS_MSN"),
|
msn: Deno.env.get("VIPPS_MSN"),
|
||||||
baseUrl: Deno.env.get("VIPPS_API_BASE_URL"),
|
baseUrl: Deno.env.get("VIPPS_API_BASE_URL"),
|
||||||
};
|
};
|
||||||
|
|
@ -33,8 +37,8 @@ const getAccessToken = async function () {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"client_id": config.clientId,
|
client_id: config.clientId,
|
||||||
"client_secret": config.clientSecret,
|
client_secret: config.clientSecret,
|
||||||
...standardVippsHeaders,
|
...standardVippsHeaders,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -56,23 +60,23 @@ export const createAgreement = async function (email: string) {
|
||||||
...standardVippsHeaders,
|
...standardVippsHeaders,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"interval": {
|
interval: {
|
||||||
"unit": "MONTH",
|
unit: "MONTH",
|
||||||
"count": 1,
|
count: 1,
|
||||||
},
|
},
|
||||||
"initialCharge": {
|
initialCharge: {
|
||||||
"amount": amount,
|
amount: amount,
|
||||||
"description": "Initial charge",
|
description: "Initial charge",
|
||||||
"transactionType": "DIRECT_CAPTURE",
|
transactionType: "DIRECT_CAPTURE",
|
||||||
},
|
},
|
||||||
"pricing": {
|
pricing: {
|
||||||
"amount": amount,
|
amount: amount,
|
||||||
"currency": "NOK",
|
currency: "NOK",
|
||||||
},
|
},
|
||||||
// email is used to identify the user in the success page
|
// email is used to identify the user in the success page
|
||||||
"merchantRedirectUrl": `${getHostUrl()}/donations-success?email=${email}`,
|
merchantRedirectUrl: `${getHostUrl()}/donations-success?email=${email}`,
|
||||||
"merchantAgreementUrl": `${getHostUrl()}`,
|
merchantAgreementUrl: `${getHostUrl()}`,
|
||||||
"productName": "Månedlig støtte til NRSS",
|
productName: "Månedlig støtte til NRSS",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -89,15 +93,23 @@ 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";
|
||||||
|
}
|
||||||
|
| Error
|
||||||
|
> {
|
||||||
const token = await getAccessToken();
|
const token = await getAccessToken();
|
||||||
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, {
|
const response = await fetch(
|
||||||
// @ts-ignore
|
`${config.baseUrl}/recurring/v3/agreements/${agreementId}`,
|
||||||
headers: {
|
{
|
||||||
authorization: `Bearer ${token}`,
|
// @ts-ignore
|
||||||
...standardVippsHeaders,
|
headers: {
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
...standardVippsHeaders,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
if (response.status === STATUS_CODE.OK) {
|
if (response.status === STATUS_CODE.OK) {
|
||||||
return response.json();
|
return response.json();
|
||||||
|
|
@ -110,18 +122,21 @@ export const getAgreement = async function (agreementId: string) {
|
||||||
|
|
||||||
export const cancelAgreement = async function (agreementId: string) {
|
export const cancelAgreement = async function (agreementId: string) {
|
||||||
const token = await getAccessToken();
|
const token = await getAccessToken();
|
||||||
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, {
|
const response = await fetch(
|
||||||
method: "PATCH",
|
`${config.baseUrl}/recurring/v3/agreements/${agreementId}`,
|
||||||
// @ts-ignore
|
{
|
||||||
headers: {
|
method: "PATCH",
|
||||||
authorization: `Bearer ${token}`,
|
// @ts-ignore
|
||||||
"Idempotency-Key": `${agreementId}-${Date.now()}`,
|
headers: {
|
||||||
...standardVippsHeaders,
|
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) {
|
if (response.status === STATUS_CODE.NoContent) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { getHostUrl, validateEmail } from "../../../lib/utils.ts";
|
import { getHostUrl, validateEmail } from "../../../lib/utils.ts";
|
||||||
import { storage } from "../../../lib/storage.ts";
|
import { storage } from "../../../lib/storage.ts";
|
||||||
import { createAgreement } from "../../../lib/vipps/vipps.ts";
|
import { createAgreement } from "../../../lib/vipps/vipps.ts";
|
||||||
|
import { getAgreement } from "../../../lib/vipps/vipps.ts";
|
||||||
|
|
||||||
export const handler = async function (req: Request): Promise<Response> {
|
export const handler = async function (req: Request): Promise<Response> {
|
||||||
const email = new URLSearchParams(req.url.split("?")[1] || "").get("email");
|
const email = new URLSearchParams(req.url.split("?")[1] || "").get("email");
|
||||||
|
|
@ -21,11 +22,27 @@ export const handler = async function (req: Request): Promise<Response> {
|
||||||
const hasActiveAgreement = existingAgreement &&
|
const hasActiveAgreement = existingAgreement &&
|
||||||
existingAgreement.validAt !== null &&
|
existingAgreement.validAt !== null &&
|
||||||
existingAgreement.revokedAt === null;
|
existingAgreement.revokedAt === null;
|
||||||
|
|
||||||
|
const agreementInVipps = existingAgreement && (await getAgreement(existingAgreement.agreementId));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
agreementInVipps &&
|
||||||
|
!(agreementInVipps instanceof Error) &&
|
||||||
|
agreementInVipps.status !== "ACTIVE" &&
|
||||||
hasActiveAgreement
|
hasActiveAgreement
|
||||||
) {
|
) {
|
||||||
console.error("Agreement already exists", email);
|
// Somehow the Vipps agreement is not active in our system anymore.
|
||||||
return Response.redirect(errorPage);
|
// 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);
|
const agreement = await createAgreement(email);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue