Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize payments #779

Draft
wants to merge 8 commits into
base: supporter_level_goal
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// License: LGPL-3.0-or-later
import PlausibleCallback from './PlausibleCallback';


describe('PlausibleCallback', () => {
describe('.canRun', () => {
it('false when getPlausible is undefined', () => {
const c = new PlausibleCallback({ props: {}} as any);
expect(c.canRun()).toEqual(false)
})

it('false when getPlausible returns undefined', () => {
const c = new PlausibleCallback({ props: {getPlausible: ():any => undefined}} as any);
expect(c.canRun()).toEqual(false)
})

it('true when returns plausible function', () => {
const realPlausibleFunc = jest.fn();
const c = new PlausibleCallback({ props: {getPlausible: ():any => realPlausibleFunc}} as any);
expect(c.canRun()).toEqual(true);
})
})

describe('.run', () => {
function build(result?:{charge?:{amount?:number}}) {
const realPlausibleFunc = jest.fn();
return {
plausible: realPlausibleFunc,
obj: new PlausibleCallback({ props: {getPlausible: ():any => realPlausibleFunc}, result} as any)
};

}

it('calls plausible with no amount when result is undefined', async () => {
const {plausible, obj} = build();
await obj.run();
expect(plausible).toHaveBeenCalledWith('payment_succeeded', {
props: {
amount: undefined,
}
});
})

it('calls plausible with no amount when charge is undefined', async () => {
const {plausible, obj} = build({});
await obj.run();
expect(plausible).toHaveBeenCalledWith('payment_succeeded', {
props: {
amount: undefined,
}
});
})

it('calls plausible with no amount when charge.amount is undefined', async () => {
const {plausible, obj} = build({charge:{}});
await obj.run();
expect(plausible).toHaveBeenCalledWith('payment_succeeded', {
props: {
amount: undefined,
}
});
})

it('calls plausible with amount/100 when charge.amount is defined', async () => {
const {plausible, obj} = build({charge:{amount: 1000}});
await obj.run();
expect(plausible).toHaveBeenCalledWith('payment_succeeded', {
props: {
amount: 10,
}
});
})
});

describe('.catchError', () => {
it('does not rethrow errors', () => {
const c = new PlausibleCallback({} as any);
expect(() => c.catchError(new Error())).not.toThrow();
})
})
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// License: LGPL-3.0-or-later
import { Callback } from "../../../../../app/javascript/common/Callbacks";
import DonationSubmitter from './';

export interface PlausibleFunction {
(eventType: string, val: any): void
}

export interface GetPlausible {

(): PlausibleFunction | undefined
}


export default class PlausibleCallback extends Callback<DonationSubmitter> {

private get plausibleFunction(): PlausibleFunction {
return this.props.props.getPlausible()
}
canRun(): boolean {
return !!(this.props.props.getPlausible && this.props.props.getPlausible())
}

run(): void {
this.plausibleFunction('payment_succeeded', {
props: {
amount: this.props.result?.charge?.amount && (this.props.result.charge.amount / 100)
}
});
}

catchError(e: unknown): void {
console.log(e);
}

}
81 changes: 72 additions & 9 deletions client/js/nonprofits/donate/DonationSubmitter/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
// License: LGPL-3.0-or-later
import DonationSubmitter from '.';
import run from '../../../../../app/javascript/common/Callbacks/run';
import PlausibleCallback from './PlausibleCallback';
import {waitFor} from '@testing-library/dom';

jest.mock('../../../../../app/javascript/common/Callbacks/run', () => jest.fn());

describe('DonationSubmitter', () => {


function SetupDonationSubmitter(updated=jest.fn()) {
beforeEach(() => {
jest.clearAllMocks();
})

function SetupDonationSubmitter(updated=jest.fn(), getPlausible=jest.fn()) {
const runCallbacks = run as jest.Mock;

const ret = {
submitter: new DonationSubmitter(),
submitter: new DonationSubmitter({getPlausible}),
updated,
getPlausible,
runCallbacks,
};

ret.submitter.addEventListener('updated', ret.updated)


return ret;

}

it('has only one postSuccess callback', () => {
const ret = SetupDonationSubmitter()
expect(Array.from(ret.submitter.callbacks().keys())).toStrictEqual(['success'])

expect(ret.submitter.callbacks('success')).toStrictEqual({before: [], after: [PlausibleCallback]})
})

describe("before anything happens", () => {

function prepare(): ReturnType<typeof SetupDonationSubmitter> {
Expand Down Expand Up @@ -52,6 +72,12 @@ describe('DonationSubmitter', () => {
const {updated} = prepare()
expect(updated).not.toHaveBeenCalled()
})

it('has not ran callbacks', () => {
const {runCallbacks} = prepare();
expect(runCallbacks).not.toHaveBeenCalled();

});
})

describe("when beginSubmit and then savedCard", () => {
Expand Down Expand Up @@ -106,10 +132,16 @@ describe('DonationSubmitter', () => {

expect(updated).toHaveBeenCalledTimes(2);
})

it('has not ran callbacks', () => {
const {runCallbacks} = prepare();
expect(runCallbacks).not.toHaveBeenCalled();

});
})

describe("when beginSubmit and then completed", () => {

const donationResult = { };
function prepare(): ReturnType<typeof SetupDonationSubmitter> {
const mocked = SetupDonationSubmitter();
Expand Down Expand Up @@ -156,13 +188,19 @@ describe('DonationSubmitter', () => {
expect(updated).toHaveBeenCalledTimes(3);
})

it('calling completed twice only fires it once', () => {
it('calling completed twice only fires it once', async () => {
const {submitter: state, updated} = prepare();
state.reportCompleted(donationResult);

expect(updated).toHaveBeenCalledTimes(3)
})

it('has ran callbacks', async () => {
const {runCallbacks, submitter:state} = prepare();
expect(runCallbacks).toHaveBeenCalledWith(state, []);

await waitFor(() => expect(runCallbacks).toHaveBeenCalledWith(state, [PlausibleCallback]))
});
})

describe("when beginSubmit and then errored", () => {
Expand Down Expand Up @@ -221,6 +259,12 @@ describe('DonationSubmitter', () => {
expect(updated).toHaveBeenCalledTimes(2);

})

it('has not ran callbacks', () => {
const {runCallbacks} = prepare();
expect(runCallbacks).not.toHaveBeenCalled();

});
})

describe("when savedCard and then errored", () => {
Expand Down Expand Up @@ -278,6 +322,12 @@ describe('DonationSubmitter', () => {

expect(updated).toHaveBeenCalledTimes(3);
})

it('has not ran callbacks', () => {
const {runCallbacks} = prepare();
expect(runCallbacks).not.toHaveBeenCalled();

});
});


Expand Down Expand Up @@ -329,13 +379,18 @@ describe('DonationSubmitter', () => {
expect(updated).toHaveBeenCalledTimes(4);
});

it('has not ran callbacks', () => {
const {runCallbacks} = prepare();
expect(runCallbacks).not.toHaveBeenCalled();

});
})

describe("when errored and then re-attempted", () => {
describe("when errored and then succeeded", () => {
const error = "Error message";
const donationResult:any = { charge: undefined };
function prepare(): ReturnType<typeof SetupDonationSubmitter> {
const mocked = SetupDonationSubmitter(jest.fn());
const mocked = SetupDonationSubmitter(jest.fn(), jest.fn());
mocked.submitter.reportBeginSubmit();
mocked.submitter.reportSavedCard();
mocked.submitter.reportError(error);
Expand Down Expand Up @@ -380,6 +435,14 @@ describe('DonationSubmitter', () => {

expect(updated).toHaveBeenCalledTimes(6);
});

it('has ran callbacks', async () => {
const {runCallbacks, submitter:state} = prepare();
expect(runCallbacks).toHaveBeenCalledWith(state, []);
await waitFor(() => expect(runCallbacks).toHaveBeenCalledWith(state, [PlausibleCallback]))


});
})


Expand Down
35 changes: 31 additions & 4 deletions client/js/nonprofits/donate/DonationSubmitter/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
// License: LGPL-3.0-or-later

import StateManager, {DonationResult} from "./StateManager";
import noop from 'lodash/noop';

export default class DonationSubmitter implements EventTarget {
import StateManager, { DonationResult } from "./StateManager";
import { CallbackControllerBuilder } from '../../../../../app/javascript/common/Callbacks';


import PlausibleCallback, { GetPlausible } from './PlausibleCallback';
import type { CallbackAccessor, CallbackFilters, CallbackMap, CallbackClass } from "../../../../../app/javascript/common/Callbacks/types";

interface DonationSubmitterProps {
getPlausible?: GetPlausible,
}

type ActionNames = 'success'

export default class DonationSubmitter implements EventTarget, CallbackAccessor<DonationSubmitter, ActionNames> {


private stateManager = new StateManager();

private eventTarget = new EventTarget();

constructor() {
private callbackController = new CallbackControllerBuilder('success').withInputType<DonationSubmitter>();

constructor(public readonly props: DonationSubmitterProps) {
this.callbackController.addAfterCallback('success', PlausibleCallback);

this.stateManager.addEventListener('beginSubmit', this.handleBeginSubmit);
this.stateManager.addEventListener('savedCard', this.handleSavedCard);
Expand Down Expand Up @@ -40,7 +56,17 @@ export default class DonationSubmitter implements EventTarget {
return this.stateManager.result;
}

public reportBeginSubmit():void {
private async postSuccess(): Promise<void> {
await this.callbackController.run('success', this, noop);
}

callbacks(): CallbackMap<DonationSubmitter, ActionNames>;
callbacks(actionName: ActionNames): CallbackFilters<CallbackClass<DonationSubmitter>> | undefined;
callbacks(actionName?: ActionNames): CallbackMap<DonationSubmitter, ActionNames> | CallbackFilters<CallbackClass<DonationSubmitter>> | undefined {
return this.callbackController.callbacks(actionName);
}

public reportBeginSubmit(): void {
this.stateManager.reportBeginSubmit();
}

Expand All @@ -67,6 +93,7 @@ export default class DonationSubmitter implements EventTarget {
}

private handleCompleted = (_evt: Event) => {
this.postSuccess();
this.dispatchEvent(new Event('updated'));
}

Expand Down
Loading