Skip to content

Commit

Permalink
feat(foxy-transaction): add support for viewing and refeeding webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
pheekus committed Aug 22, 2024
1 parent 0f1d405 commit c555fcd
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 15 deletions.
116 changes: 113 additions & 3 deletions src/elements/public/Transaction/Transaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { InternalAsyncListControl } from '../../internal/InternalAsyncListControl/InternalAsyncListControl';
import type { NucleonElement } from '../NucleonElement/NucleonElement';
import type { FetchEvent } from '../NucleonElement/FetchEvent';
import type { Resource } from '@foxy.io/sdk/core';
import type { Rels } from '@foxy.io/sdk/dist/types/backend';

import { expect, fixture, waitUntil } from '@open-wc/testing';
import { Transaction } from './index';
import { InternalForm } from '../../internal/InternalForm/InternalForm';
import { html } from 'lit-html';
import { createRouter } from '../../../server/index';
import { FetchEvent } from '../NucleonElement/FetchEvent';
import { Transaction } from './index';
import { getTestData } from '../../../testgen/getTestData';
import { html } from 'lit-html';
import { stub } from 'sinon';

describe('Transaction', () => {
Expand Down Expand Up @@ -118,6 +124,21 @@ describe('Transaction', () => {
expect(new Transaction()).to.be.instanceOf(InternalForm);
});

[
'webhooks:dialog:header:copy-json',
'webhooks:dialog:header:copy-id',
'webhooks:dialog:timestamps',
'webhooks:dialog:name',
'webhooks:dialog:query',
'webhooks:dialog:encryption-key',
'webhooks:dialog:delete',
].forEach(key => {
it(`always hides ${key}`, () => {
const element = new Transaction();
expect(element.hiddenSelector.matches(key, true)).to.be.true;
});
});

it('renders a form header', () => {
const form = new Transaction();
const renderHeaderMethod = stub(form, 'renderHeader');
Expand Down Expand Up @@ -721,4 +742,93 @@ describe('Transaction', () => {
expect(control).to.have.property('form', null);
expect(control).to.have.property('item', 'foxy-shipment-card');
});

it('renders webhooks as control', async () => {
const router = createRouter();
const element = await fixture<Transaction>(html`
<foxy-transaction
href="https://demo.api/hapi/transactions/0"
@fetch=${(evt: FetchEvent) => !evt.defaultPrevented && router.handleEvent(evt)}
>
</foxy-transaction>
`);

await waitUntil(
() => {
if (!element.in({ idle: 'snapshot' })) return false;
const nucleons = element.renderRoot.querySelectorAll<NucleonElement<any>>('foxy-nucleon');
return [...nucleons].every(nucleon => nucleon.in({ idle: 'snapshot' }));
},
'',
{ timeout: 5000 }
);

const control = element.renderRoot.querySelector('[infer="webhooks"]');

expect(control).to.exist;
expect(control).to.have.property('localName', 'foxy-internal-async-list-control');
expect(control).to.have.attribute('form', 'foxy-webhook-form');
expect(control).to.have.attribute('item', 'foxy-webhook-card');
expect(control).to.have.attribute('hide-create-button');
expect(control).to.have.attribute('hide-delete-button');
expect(control).to.have.attribute('alert');
expect(control).to.have.attribute(
'first',
'https://demo.api/hapi/webhooks?store_id=0&event_resource=transaction'
);

expect(control).to.have.deep.property('itemProps', {
'resource-uri': 'https://demo.api/hapi/transactions/0',
});

expect(control).to.have.deep.property('formProps', {
'resource-uri': 'https://demo.api/hapi/transactions/0',
});
});

it('supports refeeding multiple webhooks at once', async () => {
const requests: Request[] = [];
const router = createRouter();
const element = await fixture<Transaction>(html`
<foxy-transaction
href="https://demo.api/hapi/transactions/0"
@fetch=${(evt: FetchEvent) => {
if (evt.defaultPrevented) return;
requests.push(evt.request.clone());
router.handleEvent(evt);
}}
>
</foxy-transaction>
`);

await waitUntil(
() => {
if (!element.in({ idle: 'snapshot' })) return false;
const nucleons = element.renderRoot.querySelectorAll<NucleonElement<any>>('foxy-nucleon');
return [...nucleons].every(nucleon => nucleon.in({ idle: 'snapshot' }));
},
'',
{ timeout: 5000 }
);

const control =
element.renderRoot.querySelector<InternalAsyncListControl>('[infer="webhooks"]');

expect(control).to.have.nested.property('bulkActions.0.name', 'refeed');
expect(control).to.have.nested.property('bulkActions.0.onClick').that.is.a('function');

const webhooksCollection = await getTestData<Resource<Rels.Webhooks>>(
'./hapi/webhooks',
router
);

requests.length = 0;
const webhooksArray = webhooksCollection._embedded['fx:webhooks'] as Resource<Rels.Webhook>[];
await control?.bulkActions[0].onClick(webhooksArray);

const refeedRequest = requests.find(req => req.method === 'POST');
expect(refeedRequest).to.exist;
expect(refeedRequest?.url).to.equal('https://demo.api/virtual/empty?status=200');
expect(await refeedRequest?.json()).to.deep.equal({ refeed_hooks: [0], event: 'refeed' });
});
});
107 changes: 95 additions & 12 deletions src/elements/public/Transaction/Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { PropertyDeclarations, TemplateResult } from 'lit-element';
import type { NucleonElement } from '../NucleonElement/NucleonElement';
import type { Resource } from '@foxy.io/sdk/core';
import type { Data } from './types';
import type { Rels } from '@foxy.io/sdk/backend';

import { BooleanSelector, getResourceId } from '@foxy.io/sdk/core';
import { TranslatableMixin } from '../../../mixins/translatable';
import { BooleanSelector } from '@foxy.io/sdk/core';
import { ResponsiveMixin } from '../../../mixins/responsive';
import { InternalForm } from '../../internal/InternalForm/InternalForm';
import { ifDefined } from 'lit-html/directives/if-defined';
Expand Down Expand Up @@ -38,31 +41,75 @@ export class Transaction extends Base<Data> {

getCustomerPageHref: ((href: string) => string) | null = null;

private readonly __webhooksBulkActions = [
{
name: 'refeed',
onClick: async (selection: Resource<Rels.Webhook>[]) => {
if (!this.data) return;

// TODO remove ts-expect-error when SDK has the types
// @ts-expect-error SDK types are incomplete
const url = this.data._links['fx:send_webhooks'].href;
const api = new Transaction.API(this);
const response = await api.fetch(url, {
method: 'POST',
body: JSON.stringify({
refeed_hooks: selection.map(hook => getResourceId(hook._links['self'].href)),
event: 'refeed',
}),
});

selection.forEach(hook => {
Transaction.Rumour('').share({
related: [
...selection.map(hook => hook._links['fx:logs'].href),
...selection.map(hook => hook._links['fx:statuses'].href),
],
source: hook._links.self.href,
data: hook,
});
});

if (!response.ok) throw new Error(await response.text());
},
},
];

get readonlySelector(): BooleanSelector {
const alwaysMatch = ['billing-addresses', 'webhooks:dialog:url', super.readonlySelector];
const isEditable = Boolean(this.data?._links['fx:void'] ?? this.data?._links['fx:refund']);
return isEditable
? new BooleanSelector(`${super.readonlySelector} billing-addresses`)
: new BooleanSelector(
`${super.readonlySelector} billing-addresses items attributes custom-fields`
);
if (!isEditable) alwaysMatch.push('items', 'attributes', 'custom-fields');
return new BooleanSelector(alwaysMatch.join(' ').trim());
}

get hiddenSelector(): BooleanSelector {
const hidden = ['billing-addresses:dialog:delete billing-addresses:dialog:timestamps'];
const alwaysMatch = [
'billing-addresses:dialog:delete',
'billing-addresses:dialog:timestamps',
'webhooks:dialog:header:copy-json',
'webhooks:dialog:header:copy-id',
'webhooks:dialog:timestamps',
'webhooks:dialog:name',
'webhooks:dialog:query',
'webhooks:dialog:encryption-key',
'webhooks:dialog:delete',
super.readonlySelector,
];

const type = this.data?.type;

if (!this.data?._links['fx:subscription']) hidden.push('subscription');
if (type === 'subscription_modification') hidden.push('actions');
if (!this.data?._links['fx:subscription']) alwaysMatch.unshift('subscription');
if (type === 'subscription_modification') alwaysMatch.unshift('actions');

if (type === 'updateinfo') {
hidden.push('not=customer,subscription,payments,custom-fields,attributes');
alwaysMatch.unshift('not=customer,subscription,payments,custom-fields,attributes');
}

if (type === 'subscription_cancellation') {
hidden.push('not=customer,subscription,custom-fields,attributes');
alwaysMatch.unshift('not=customer,subscription,custom-fields,attributes');
}

return new BooleanSelector(`${super.hiddenSelector} ${hidden.join(' ')}`.trim());
return new BooleanSelector(alwaysMatch.join(' ').trim());
}

get headerSubtitleOptions(): Record<string, unknown> {
Expand Down Expand Up @@ -145,6 +192,7 @@ export class Transaction extends Base<Data> {

renderBody(): TemplateResult {
let shipmentsLink: string | undefined = undefined;
let webhooksLink: string | undefined = undefined;
let itemsLink: string | undefined = undefined;

const alertStatuses = ['problem', 'pending_fraud_review', 'rejected', 'declined'];
Expand All @@ -153,10 +201,17 @@ export class Transaction extends Base<Data> {
if (this.data) {
try {
const shipmentsUrl = new URL(this.data._links['fx:shipments'].href);
// TODO: Remove the ts-expect-error comment when SDK has the types
// @ts-expect-error SDK doesn't have the types
const webhooksUrl = new URL(this.__storeLoader?.data._links['fx:webhooks'].href ?? '');
const itemsUrl = new URL(this.data._links['fx:items'].href);

shipmentsUrl.searchParams.set('zoom', 'items:item_category');
webhooksUrl.searchParams.set('event_resource', 'transaction');
itemsUrl.searchParams.set('zoom', 'item_options');

shipmentsLink = shipmentsUrl.toString();
webhooksLink = webhooksUrl.toString();
itemsLink = itemsUrl.toString();
} catch {
//
Expand Down Expand Up @@ -254,6 +309,34 @@ export class Transaction extends Base<Data> {
item="foxy-shipment-card"
>
</foxy-internal-async-list-control>
<foxy-internal-async-list-control
infer="webhooks"
first=${ifDefined(webhooksLink)}
item="foxy-webhook-card"
form="foxy-webhook-form"
hide-create-button
hide-delete-button
alert
.bulkActions=${this.__webhooksBulkActions}
.itemProps=${{ 'resource-uri': this.href }}
.formProps=${{ 'resource-uri': this.href }}
>
</foxy-internal-async-list-control>
<foxy-nucleon
class="hidden"
infer=""
href=${ifDefined(this.data?._links['fx:store'].href)}
id="storeLoader"
@update=${() => this.requestUpdate()}
>
</foxy-nucleon>
`;
}

private get __storeLoader() {
type Loader = NucleonElement<Resource<Rels.Store>>;
return this.renderRoot.querySelector<Loader>('#storeLoader');
}
}
3 changes: 3 additions & 0 deletions src/elements/public/Transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import '../AttributeForm/index';
import '../CustomFieldForm/index';
import '../CustomFieldCard/index';
import '../CopyToClipboard/index';
import '../NucleonElement/index';
import '../ShipmentCard/index';
import '../PaymentCard/index';
import '../WebhookCard/index';
import '../WebhookForm/index';
import '../ItemCard/index';
import '../ItemForm/index';
import '../I18n/index';
Expand Down
2 changes: 2 additions & 0 deletions src/server/hapi/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const links: Links = {
'fx:shipments': { href: `./shipments?transaction_id=${id}` },
'fx:attributes': { href: `./transaction_attributes?transaction_id=${id}` },
'fx:send_emails': { href: 'https://demo.api/virtual/empty?status=200' },
'fx:send_webhooks': { href: 'https://demo.api/virtual/empty?status=200' },
'fx:subscription': { href: `./subscriptions/${subscription_id}` },
'fx:applied_taxes': { href: `./applied_taxes?transaction_id=${id}` },
'fx:custom_fields': { href: `./custom_fields?transaction_id=${id}` },
Expand Down Expand Up @@ -149,6 +150,7 @@ export const links: Links = {
'fx:users': { href: `./users?store_id=${id}` },
'fx:taxes': { href: `./taxes?store_id=${id}` },
'fx:coupons': { href: `./coupons?store_id=${id}` },
'fx:webhooks': { href: `./webhooks?store_id=${id}` },
'fx:customers': { href: `./customers?store_id=${id}` },
'fx:attributes': { href: `./store_attributes?store_id=${id}` },
'fx:transactions': { href: `./transactions?store_id=${id}` },
Expand Down
Loading

0 comments on commit c555fcd

Please sign in to comment.