Skip to content

Commit

Permalink
Membership Views, #61
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiancook committed Jul 29, 2023
1 parent a179b23 commit cbb4bdc
Show file tree
Hide file tree
Showing 17 changed files with 595 additions and 71 deletions.
81 changes: 76 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,34 @@ export interface Location extends LocationData {
updatedAt: string;
}

export type MembershipStatus = "active" | "inactive";

export interface MembershipHistoryItem {
status: MembershipStatus;
statusAt?: string;
updatedAt: string;
}

export interface MembershipData extends Record<string, unknown> {
status?: MembershipStatus;
reference?: string;
name?: string;
email?: string;
timezone?: string;
history?: MembershipHistoryItem[];
}

export interface Membership extends MembershipData {
status: MembershipStatus;
reference: string;
membershipId: string;
createdAt: string;
createdByPartnerId?: string;
createdByUserId?: string;
}

export type PartialMembership = MembershipData & Partial<Membership>;

export type MaybeNumberString = `${number}` | string;

export interface OfferPrice {
Expand Down Expand Up @@ -608,17 +636,25 @@ export type PaymentType =
| "realtime";
export type PaymentStatus = "pending" | "processing" | "paid" | "void";

export interface PaymentData extends Record<string, unknown> {
export interface Amount {
amount: string;
currency: string;
}

export interface PaymentData extends PaymentMethodIdentifier, Record<string, unknown> {
type: PaymentType;
status: PaymentStatus;
paymentMethodId: string;
totalAmount?: Amount;
reference?: string;
userId?: string;
organisationId?: string;
}

export interface Payment extends PaymentData {
export interface PaymentIdentifier extends PaymentMethodIdentifier {
paymentId: string;
}

export interface Payment extends PaymentIdentifier, PaymentData {
createdAt: string;
updatedAt: string;
}
Expand All @@ -629,11 +665,16 @@ export type PaymentMethodType =

export type PaymentMethodStatus = "pending" | "available" | "expired" | "void";


export interface PaymentMethodOwnerIdentifiers {
userId?: string;
organisationId?: string;
}

export interface PaymentMethodIdentifier extends PaymentMethodOwnerIdentifiers {
paymentMethodId: string;
}

export interface PaymentMethodData extends Record<string, unknown>, PaymentMethodOwnerIdentifiers {
status: PaymentMethodStatus;
type: PaymentMethodType;
Expand All @@ -644,8 +685,38 @@ export interface PaymentMethodData extends Record<string, unknown>, PaymentMetho
to?: ShipmentTo;
}

export interface PaymentMethod extends PaymentMethodData {
paymentMethodId: string;
export interface PaymentMethod extends PaymentMethodData, PaymentMethodIdentifier {
createdAt: string;
updatedAt: string;
}

export type PaymentRequestType =
| "invoice"
| "realtime";

export type PaymentRequestStatus = "pending" | "accepted" | "expired" | "void";

export interface PaymentRequestOwnerIdentifiers {
userId?: string;
organisationId?: string;
}

export interface PaymentRequestIdentifier extends PaymentRequestOwnerIdentifiers {
paymentRequestId: string;
}

export interface PaymentRequestData extends Record<string, unknown>, PaymentRequestOwnerIdentifiers {
status?: PaymentRequestStatus;
types?: PaymentRequestType[];
paymentMethodId?: string;
to?: ShipmentTo;
from?: ShipmentFrom;
totalAmount?: Amount;
}

export interface PaymentRequest extends PaymentRequestData, PaymentRequestIdentifier {
status: PaymentRequestStatus;
types: PaymentRequestType[];
createdAt: string;
updatedAt: string;
}
Expand Down
2 changes: 2 additions & 0 deletions src/client/interface.readonly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export * from "../data/identifier/types"
export * from "../data/inventory/types"
export * from "../data/inventory-item/types"
export * from "../data/location/types"
export * from "../data/membership/types"
export * from "../data/offer/types"
export * from "../data/order/types"
export * from "../data/order-item/types"
export * from "../data/organisation/types"
export * from "../data/partner/types"
export * from "../data/payment/types"
export * from "../data/payment-method/types"
export * from "../data/payment-request/types"
export * from "../data/product/types"
export * from "../data/shipment/types"
export * from "../data/system-log/types"
Expand Down
8 changes: 7 additions & 1 deletion src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type {ComponentConfig} from "../react/server/paths/config";
import type {ProcessChangeConfig} from "../data";
import type {StorageConfig} from "../data/storage/kv-base";
import type {LayoutConfig} from "../react/server";
import type {SetMembershipConfig} from "../data";
import type {MembershipViewComponentConfig} from "../react/server/paths/membership/view";
import type {MembershipStatusConfig} from "../data/membership/membership-status";

export interface LogisticsConfig {
routes?: FastifyPluginAsync
Expand All @@ -20,6 +23,9 @@ export interface Config extends
KeyValueStoreConfig,
ProcessChangeConfig,
StorageConfig,
LayoutConfig {
LayoutConfig,
SetMembershipConfig,
MembershipViewComponentConfig,
MembershipStatusConfig {

}
13 changes: 13 additions & 0 deletions src/data/membership/membership-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {MembershipStatus} from "./types";
import {getConfig} from "../../config";

export const DEFAULT_MEMBERSHIP_STATUS: MembershipStatus = "active";

export interface MembershipStatusConfig {
DEFAULT_MEMBERSHIP_STATUS?: MembershipStatus;
}

export function getDefaultMembershipStatus() {
const config = getConfig();
return config.DEFAULT_MEMBERSHIP_STATUS || DEFAULT_MEMBERSHIP_STATUS;
}
42 changes: 41 additions & 1 deletion src/data/membership/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@

const statusEnum = [
"active",
"inactive"
];

export const membershipHistoryItem = {
type: "object",
properties: {
status: {
type: "string",
nullable: true,
enum: statusEnum
},
statusAt: {
type: "string",
nullable: true
},
updatedAt: {
type: "string"
}
},
required: ["updatedAt"]
}

export const membershipData = {
type: "object",
properties: {
Expand All @@ -20,6 +45,16 @@ export const membershipData = {
type: "string",
nullable: true
},
status: {
type: "string",
enum: statusEnum,
nullable: true
},
history: {
type: "array",
items: membershipHistoryItem,
nullable: true
}
},
additionalProperties: true,
required: [
Expand All @@ -37,11 +72,16 @@ export const membership = {
createdAt: {
type: "string",
},
status: {
type: "string",
enum: statusEnum
},
},
additionalProperties: true,
required: [
...membershipData.required,
"membershipId",
"createdAt"
"createdAt",
"status"
]
}
102 changes: 42 additions & 60 deletions src/data/membership/set-membership.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,72 @@
import {getMembershipStore} from "./store";
import {getMembershipIdentifierCounterStore, getMembershipStore} from "./store";
import {Membership, PartialMembership} from "./types";
import {createHash} from "crypto";
import {getMembership} from "./get-membership";
import {entries} from "../entries";
import {getMaybePartner, getMaybeUser, isUnauthenticated} from "../../authentication";
import {getMaybePartner, getMaybeUser} from "../../authentication";
import {v4} from "uuid";
import {ok} from "../../is";
import {isNumberString} from "../../is";
import {getConfig} from "../../config";
import {getDefaultMembershipStatus} from "./membership-status";

export interface SetMembershipConfig {
createMembershipId?(data: PartialMembership): string;
createMembershipReference?(data: PartialMembership): string | Promise<string>;
}

const {
ATTENDEE_DISABLE_PARTITION,
ATTENDEE_PARTITION,
MEMBERSHIP_IDENTIFIER_COUNTER = "counter",
MEMBERSHIP_REFERENCE_PREFIX = "",
MEMBERSHIP_REFERENCE_LENGTH
} = process.env;

function getPartitionPrefix() {
if (ATTENDEE_PARTITION) {
return ATTENDEE_PARTITION;
}
const partner = getMaybePartner();
// If authenticated, membership information will be retained across happenings
if (partner?.partnerId) {
return `partner:${partner.partnerId}:`
}
const user = getMaybeUser();
if (user?.userId) {
// Users can create their own memberships
return `user:${user.userId}:`
}
ok(isUnauthenticated(), "Expected user or partner if not anonymous");
// Random every time if no authentication :)
// If creating a happening tree, each new tree request will have a new set of memberships
return v4();
}
const DEFAULT_MEMBERSHIP_REFERENCE_LENGTH = 6;

/**
* Allows partial update of an membership, retains existing properties
* @param data
*/
export async function setMembership(data: PartialMembership) {
const config = getConfig();
const store = getMembershipStore();
let reference = data.reference,
membershipId = data.membershipId;
// Allows for either reference or membershipId to be provided as a reference string
if (!membershipId) {
const existing = await getMembership(reference);
if (existing) {
reference = existing.reference;
membershipId = existing.membershipId;
}
}
if (!membershipId) {
membershipId = createMembershipId();
}
const existing = await getMembership(membershipId);
if (existing && !isDifferent(existing)) {
return existing;
let reference = data.reference
if (!reference) {
reference = await createMembershipReference();
}
const membershipId = data.membershipId || v4();
const createdAt = data.createdAt || new Date().toISOString();
const updatedAt = new Date().toISOString();
const createdByPartnerId = getMaybePartner()?.partnerId;
const createdByUserId = getMaybeUser()?.userId;
const status = data.status || getDefaultMembershipStatus();
const membership: Membership = {
...existing,
...data,
history: data.history || [
{
status,
statusAt: updatedAt,
updatedAt
}
],
status,
reference,
membershipId,
createdAt,
createdByPartnerId,
createdByUserId
createdByUserId,
updatedAt
};
await store.set(membershipId, membership);
return membership;

function isDifferent(value: Membership) {
return !!entries(data).find(entry => value[entry[0]] !== entry[1]);
}

function createMembershipId() {
if (ATTENDEE_DISABLE_PARTITION) {
return data.reference;
async function createMembershipReference() {
if (config.createMembershipReference) {
return config.createMembershipReference(data);
}
const hash = createHash("sha512");
hash.update(getPartitionPrefix());
hash.update(data.reference);
return hash.digest().toString("hex");
const counter = await incrementMembershipIdentifier();
const length = isNumberString(MEMBERSHIP_REFERENCE_LENGTH) ? +MEMBERSHIP_REFERENCE_LENGTH : DEFAULT_MEMBERSHIP_REFERENCE_LENGTH;
return `${MEMBERSHIP_REFERENCE_PREFIX}${counter.toString().padStart(length, "0")}`;
}
}

export async function addMembership(data: PartialMembership) {
return setMembership(data);
}

export async function incrementMembershipIdentifier() {
const store = await getMembershipIdentifierCounterStore();
return await store.increment(MEMBERSHIP_IDENTIFIER_COUNTER);
}
7 changes: 7 additions & 0 deletions src/data/membership/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import {getKeyValueStore} from "../kv";
import {Membership} from "./types";

const STORE_NAME = "membership" as const;
const IDENTIFIERS_STORE_NAME = `${STORE_NAME}:IdentifiersCounter` as const;

export function getMembershipStore() {
return getKeyValueStore<Membership>(STORE_NAME, {
counter: true
});
}

export function getMembershipIdentifierCounterStore() {
return getKeyValueStore<number>(IDENTIFIERS_STORE_NAME, {
counter: false // Disables secondary counting on updates
})
}
Loading

0 comments on commit cbb4bdc

Please sign in to comment.