Skip to content

Commit

Permalink
feat: add Backend.getNextTransactionDateConstraints helper
Browse files Browse the repository at this point in the history
  • Loading branch information
pheekus committed Mar 25, 2021
1 parent c7141ac commit cde4cae
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/backend/getNextTransactionDateConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type * as Core from '../core';
import type * as Rels from './Rels';

import { getTimeFromFrequency } from './getTimeFromFrequency.js';
import jsonata from 'jsonata';

type Rules = Core.Resource<Rels.CustomerPortalSettings>['subscriptions']['allowNextDateModification'];
type Subscription = Omit<Core.Resource<Rels.Subscription>, '_links' | '_embedded'>;

export type Constraints = {
min?: string;
max?: string;
disallowedDates?: string[];
allowedDaysOfWeek?: number[];
allowedDaysOfMonth?: number[];
};

/**
* Finds which next transaction date modification rules are applicable to
* the given subscription and merges them together.
*
* @param data Subscription to generate constraints for.
* @param rules Next date modification config from the customer portal settings.
*
* @returns
* Returns true if all modifications are allowed, false if next date can't
* be changed by the customer, object with constraints in any other case.
*/
export function getNextTransactionDateConstraints(data: Subscription, rules: Rules): Constraints | boolean {
if (typeof rules === 'boolean') return rules;

const combinedRule = rules
.filter(rule => Boolean(jsonata(rule.jsonataQuery).evaluate(data)))
.reduce((result, rule) => {
if (rule.min) {
const currentTime = result.min ? getTimeFromFrequency(result.min) : Infinity;
const proposedTime = getTimeFromFrequency(rule.min);
if (proposedTime < currentTime) result.min = rule.min;
}

if (rule.max) {
const currentTime = result.max ? getTimeFromFrequency(result.max) : -Infinity;
const proposedTime = getTimeFromFrequency(rule.max);
if (proposedTime > currentTime) result.max = rule.max;
}

if (rule.allowedDays?.type === 'day') {
const previousSet = result.allowedDaysOfWeek ?? [];
const expandedSet = [...previousSet, ...rule.allowedDays.days];
result.allowedDaysOfWeek = Array.from(new Set(expandedSet));
}

if (rule.allowedDays?.type === 'month') {
const previousSet = result.allowedDaysOfMonth ?? [];
const expandedSet = [...previousSet, ...rule.allowedDays.days];
result.allowedDaysOfMonth = Array.from(new Set(expandedSet));
}

if (rule.disallowedDates) {
const previousSet = result.disallowedDates ?? [];
const expandedSet = [...previousSet, ...rule.disallowedDates];
result.disallowedDates = Array.from(new Set(expandedSet));
}

return result;
}, {} as Constraints);

return Object.keys(combinedRule).length === 0 ? false : combinedRule;
}
1 change: 1 addition & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { API } from './API.js';
export { Signer } from './Signer.js';
export { verifyWebhookSignature } from './verifyWebhookSignature.js';
export { getAllowedFrequencies } from './getAllowedFrequencies.js';
export { getNextTransactionDateConstraints } from './getNextTransactionDateConstraints.js';
export { getTimeFromFrequency, InvalidFrequencyError } from './getTimeFromFrequency.js';
export { isNextTransactionDate } from './isNextTransactionDate.js';
export { createSSOURL } from './createSSOURL.js';
Expand Down
82 changes: 82 additions & 0 deletions tests/backend/getNextTransactionDateConstraints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { getNextTransactionDateConstraints as getConstraints } from '../../src/backend';

const sampleData = {
date_created: '2013-08-19T10:58:39-0700',
date_modified: '2013-08-19T10:58:39-0700',
end_date: null,
error_message: '',
first_failed_transaction_date: null,
frequency: '1m',
is_active: false,
next_transaction_date: '2014-05-01T00:00:00-0700',
past_due_amount: 0,
start_date: '2010-09-15T00:00:00-0700',
third_party_id: '',
};

describe('Backend', () => {
describe('getNextTransactionDateConstraints', () => {
it('returns false if rules argument is false', () => {
expect(getConstraints(sampleData, false)).toBe(false);
});

it('returns false if none of the rules apply', () => {
const rules = [{ jsonataQuery: '$contains(frequency, "d")' }, { jsonataQuery: '$contains(frequency, "d")' }];
expect(getConstraints(sampleData, rules)).toBe(false);
});

it('returns true if rules argument is true', () => {
expect(getConstraints(sampleData, true)).toBe(true);
});

it('picks the smallest of the applicable min properties', () => {
const rules = [
{ jsonataQuery: '$contains(frequency, "m")', min: '4y' },
{ jsonataQuery: '$contains(frequency, "m")', min: '2w' },
{ jsonataQuery: '$contains(frequency, "d")', min: '1d' },
];

expect(getConstraints(sampleData, rules)).toHaveProperty('min', '2w');
});

it('picks the greatest of the applicable max properties', () => {
const rules = [
{ jsonataQuery: '$contains(frequency, "m")', max: '2w' },
{ jsonataQuery: '$contains(frequency, "m")', max: '4m' },
{ jsonataQuery: '$contains(frequency, "d")', max: '7y' },
];

expect(getConstraints(sampleData, rules)).toHaveProperty('max', '4m');
});

it('combines and dedupes all applicable disallowed dates', () => {
const rules = [
{ disallowedDates: ['2020-03-14'], jsonataQuery: '$contains(frequency, "m")' },
{ disallowedDates: ['2020-03-14', '2021-02-18'], jsonataQuery: '$contains(frequency, "m")' },
{ disallowedDates: ['2019-03-26'], jsonataQuery: '$contains(frequency, "d")' },
];

expect(getConstraints(sampleData, rules)).toHaveProperty('disallowedDates', ['2020-03-14', '2021-02-18']);
});

it('combines and dedupes all applicable allowed days of week', () => {
const rules = [
{ allowedDays: { days: [1], type: 'day' as const }, jsonataQuery: '$contains(frequency, "m")' },
{ allowedDays: { days: [1, 2], type: 'day' as const }, jsonataQuery: '$contains(frequency, "m")' },
{ allowedDays: { days: [3], type: 'day' as const }, jsonataQuery: '$contains(frequency, "d")' },
];

expect(getConstraints(sampleData, rules)).toHaveProperty('allowedDaysOfWeek', [1, 2]);
});

it('combines and dedupes all applicable allowed days of month', () => {
const rules = [
{ allowedDays: { days: [23], type: 'month' as const }, jsonataQuery: '$contains(frequency, "m")' },
{ allowedDays: { days: [23, 31], type: 'month' as const }, jsonataQuery: '$contains(frequency, "m")' },
{ allowedDays: { days: [10], type: 'month' as const }, jsonataQuery: '$contains(frequency, "d")' },
];

expect(getConstraints(sampleData, rules)).toHaveProperty('allowedDaysOfMonth', [23, 31]);
});
});
});

0 comments on commit cde4cae

Please sign in to comment.