Skip to content

Commit

Permalink
Zapier new invoice trigger (#86)
Browse files Browse the repository at this point in the history
This trigger fires when a new invoice in created.
  • Loading branch information
Jsnxyz authored Apr 26, 2024
1 parent 0bfaa70 commit f1749f1
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 8 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cobot-zapier",
"version": "2.1.0",
"version": "2.2.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const AUTHORIZE_URL = `${BASE_URL}/oauth/authorize`;
const ACCESS_TOKEN_URL = `${BASE_URL}/oauth/access_token`;
const TEST_AUTH_URL = `${BASE_URL}/api/user`;
const scopes =
"read read_admins read_bookings read_events read_external_bookings read_memberships read_resources read_spaces read_user write_activities write_subscriptions";
"read read_admins read_bookings read_events read_external_bookings read_invoices read_memberships read_resources read_spaces read_user write_activities write_subscriptions";

const getAccessToken = async (
z: ZObject,
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import triggerMembershipPlanChanged from "./triggers/triggerMembershipPlanChange
import triggerExternalBooking from "./triggers/triggerExternalBookingCreated";
import getSubdomains from "./triggers/dropdowns/getSubdomains";
import triggerEventPublished from "./triggers/triggerEventPublished";
import triggerInvoiceCreated from "./triggers/triggerInvoiceCreated";

const { version } = require("../package.json");

Expand All @@ -32,6 +33,7 @@ export default {
[triggerMembershipConfirmed.key]: triggerMembershipConfirmed,
[triggerMembershipPlanChanged.key]: triggerMembershipPlanChanged,
[triggerEventPublished.key]: triggerEventPublished,
[triggerInvoiceCreated.key]: triggerInvoiceCreated,
[triggerExternalBooking.key]: triggerExternalBooking,
// Lists for dropdowns
[getSubdomains.key]: getSubdomains,
Expand Down
148 changes: 148 additions & 0 deletions src/test/triggers/triggerInvoiceCreated.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { createAppTester } from "zapier-platform-core";
import * as nock from "nock";
import App from "../../index";
import {
prepareBundle,
prepareMocksForWebhookSubscribeTest,
} from "../utils/prepareMocksForWebhookSubscribeTest";
import triggerInvoiceCreated from "../../triggers/triggerInvoiceCreated";
import { HookTrigger } from "../../types/trigger";
import {
UserApiResponse,
InvoiceApiResponse,
BaseInvoiceProperties,
} from "../../types/api-responses";

const attributes: BaseInvoiceProperties = {
invoiceDate: "2024-12-20T06:22:29+01:00",
paidStatus: "paid",
dueDate: "2024-12-30T08:22:29+01:00",
number: "1",
sentStatus: "sent",
taxIdName: "taxIdName",
canCharge: true,
recipientAddress: {
company: "company",
name: "name",
fullAddress: "fullAddress",
},
senderAddress: {
company: "company",
name: "name",
fullAddress: "fullAddress",
},
items: [
{
description: "item 1",
quantity: "1",
paid: true,
accountingCode: null,
amount: {
net: "100",
gross: "100",
currency: "EUR",
taxes: [],
},
totalAmount: {
net: "100",
gross: "100",
currency: "EUR",
taxes: [],
},
},
],
payableAmount: "100",
paidAmount: "100",
totalAmount: {
net: "100",
gross: "100",
currency: "EUR",
taxes: [],
},
invoiceText: "invoiceText",
paidDate: "2024-12-22T06:22:29+01:00",
taxId: null,
chargeAt: null,
customerNumber: null,
notes: null,
};

const invoiceResponse: InvoiceApiResponse = {
id: "1",
attributes,
relationships: {
membership: {
data: {
id: "membership-1",
},
},
},
};

const appTester = createAppTester(App);
nock.disableNetConnect();
const trigger = App.triggers[triggerInvoiceCreated.key] as HookTrigger;

afterEach(() => nock.cleanAll());

describe("triggerInvoiceCreated", () => {
it("creates new webhook through CM API upon subscribe", async () => {
const bundle = prepareMocksForWebhookSubscribeTest(
triggerInvoiceCreated.key,
);
const subscribe = trigger.operation.performSubscribe;

const result = await appTester(subscribe as any, bundle as any);

expect(result).toMatchInlineSnapshot(`
{
"url": "https://trial.cobot.me/api/event/callback",
}
`);
});

it("lists recent invoices", async () => {
const bundle = prepareBundle();
const userResponse: UserApiResponse = {
included: [{ id: "space-1", attributes: { subdomain: "trial" } }],
};
const scope = nock("https://api.cobot.me");
scope.get("/user?include=adminOf").reply(200, userResponse);
scope
.get(/\/spaces\/space-1\/invoices/)
.reply(200, { data: [invoiceResponse] });

const listRecentEvents = trigger.operation.performList;

const results = await appTester(listRecentEvents as any, bundle as any);

expect(results).toStrictEqual([
{
...attributes,
id: "1",
membershipId: "membership-1",
},
]);
});

it("triggers on new invoice", async () => {
const bundle = prepareBundle();
const scope = nock("https://api.cobot.me");
scope.get("/invoices/12345").reply(200, { data: invoiceResponse });

const results = await appTester(
triggerInvoiceCreated.operation.perform as any,
bundle as any,
);

expect(nock.isDone()).toBe(true);

expect(results).toStrictEqual([
{
...attributes,
id: "1",
membershipId: "membership-1",
},
]);
});
});
3 changes: 3 additions & 0 deletions src/test/utils/prepareMocksForWebhookSubscribeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const prepareBundle = (): KontentBundle<SubscribeBundleInputType> => {
...mockBundle.inputData,
subdomain: "trial",
},
cleanedRequest: {
url: "https://trial.cobot.me/api/event/12345",
},
targetUrl: "https://test-url.test",
};

Expand Down
78 changes: 78 additions & 0 deletions src/triggers/triggerInvoiceCreated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ZObject } from "zapier-platform-core";
import { KontentBundle } from "../types/kontentBundle";
import {
listRecentInvoices,
subscribeHook,
unsubscribeHook,
getInvoiceFromApi2,
} from "../utils/api";
import { SubscribeBundleInputType } from "../types/subscribeType";
import { getSubdomainField } from "../fields/getSudomainsField";
import { invoiceSample } from "../utils/samples";
import { InvoiceApiResponse } from "../types/api-responses";
import { InvoiceOutput } from "../types/outputs";
import { HookTrigger } from "../types/trigger";
import { apiResponseToInvoiceOutput } from "../utils/api-to-output";

const hookLabel = "Invoice Created";
const event = "created_invoice";

async function subscribeHookExecute(
z: ZObject,
bundle: KontentBundle<SubscribeBundleInputType>,
) {
return subscribeHook(z, bundle, {
event,
callback_url: bundle.targetUrl ?? "",
});
}

async function unsubscribeHookExecute(
z: ZObject,
bundle: KontentBundle<SubscribeBundleInputType>,
) {
const webhook = bundle.subscribeData;
return unsubscribeHook(z, bundle, webhook?.id ?? "");
}

async function parsePayload(
z: ZObject,
bundle: KontentBundle<{}>,
): Promise<InvoiceOutput[]> {
const invoiceId = bundle.cleanedRequest.url.split("/").pop();
const response = await getInvoiceFromApi2(z, invoiceId);
if (response) {
return [apiResponseToInvoiceOutput(response)];
} else {
return [];
}
}

const trigger: HookTrigger = {
key: `${event}`,
noun: hookLabel,
display: {
label: hookLabel,
description: "Triggers when an invoice is created.",
},
operation: {
type: "hook",

inputFields: [getSubdomainField()],

performSubscribe: subscribeHookExecute,
performUnsubscribe: unsubscribeHookExecute,

perform: parsePayload,
performList: async (
z: ZObject,
bundle: KontentBundle<SubscribeBundleInputType>,
): Promise<InvoiceOutput[]> => {
const invoices = await listRecentInvoices(z, bundle);
return invoices.map((invoice) => apiResponseToInvoiceOutput(invoice));
},

sample: invoiceSample,
},
};
export default trigger;
51 changes: 51 additions & 0 deletions src/types/api-responses.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,57 @@ export type EventApiResponse = {
};
};

type Address = {
company: string | null;
name: string | null;
fullAddress: string | null;
};

type InvoiceItem = {
description: string;
paid: boolean;
quantity: string;
accountingCode: string | null;
amount: Amount;
totalAmount: Amount;
};

type Relationship = {
data: {
id: string;
};
};

export type BaseInvoiceProperties = {
invoiceText: string | null;
invoiceDate: string;
paidDate: string | null;
paidStatus: "unpaid" | "paid" | "pending" | "canceled" | "writtenOff";
dueDate: string;
number: string;
sentStatus: "sent" | "sending" | "unsent" | "failedToSend";
taxId: string | null;
taxIdName: string;
chargeAt: string | null;
canCharge: boolean;
customerNumber: string | null;
recipientAddress: Address;
senderAddress: Address;
notes: string | null;
items: InvoiceItem[];
payableAmount: string;
paidAmount: string;
totalAmount: Amount;
};

export type InvoiceApiResponse = {
id: string;
attributes: BaseInvoiceProperties;
relationships: {
membership: Relationship;
};
};

export type ExternalBookingStatus = "approved" | "pending" | "canceled";

export type ExternalBookingApiResponse = {
Expand Down
7 changes: 6 additions & 1 deletion src/types/outputs.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExternalBookingStatus } from "./api-responses";
import { ExternalBookingStatus, BaseInvoiceProperties } from "./api-responses";

export type BookingOutput = {
id: string;
Expand Down Expand Up @@ -57,3 +57,8 @@ export type MembershipOutput = {
plan_name: string;
payment_method_name: string | null;
};

export type InvoiceOutput = BaseInvoiceProperties & {
membershipId?: string;
id: string;
};
13 changes: 13 additions & 0 deletions src/utils/api-to-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import {
BookingApiResponse,
EventApiResponse,
MembershipApiResponse,
InvoiceApiResponse,
} from "../types/api-responses";
import {
BookingOutput,
EventOutput,
ExternalBookingOutput,
MembershipOutput,
InvoiceOutput,
} from "../types/outputs";
import { ExternalBookingWithResourceApiResponse } from "./api";

Expand All @@ -25,6 +27,17 @@ export function apiResponseToMembershipOutput(
};
}

export function apiResponseToInvoiceOutput(
invoice: InvoiceApiResponse,
): InvoiceOutput {
const attributes = invoice.attributes;
return {
...attributes,
id: invoice.id,
membershipId: invoice.relationships?.membership?.data?.id ?? undefined,
};
}

export function apiResponseToEventOutput(event: EventApiResponse): EventOutput {
const attributes = event.attributes;
return {
Expand Down
Loading

0 comments on commit f1749f1

Please sign in to comment.