diff --git a/Card/Event/Authorization.ts b/Card/Event/Authorization.ts new file mode 100644 index 00000000..d6f8e372 --- /dev/null +++ b/Card/Event/Authorization.ts @@ -0,0 +1,24 @@ +import { isoly } from "isoly" +import { isly } from "isly" +import { Amount } from "../../Amount" + +export interface Authorization { + type: "authorization" + id: string + outcome: Authorization.Outcome + at: isoly.DateTime + amount: Amount + reason?: string +} +export namespace Authorization { + export const outcomes = ["created", "rejected", "cancelled"] as const + export type Outcome = typeof outcomes[number] + export const type = isly.object({ + type: isly.string("authorization"), + id: isly.string(), + outcome: isly.string(outcomes), + at: isly.fromIs("isoly.DateTime", isoly.DateTime.is), + amount: Amount.type, + reason: isly.string().optional(), + }) +} diff --git a/Card/Event/Cancel.ts b/Card/Event/Cancel.ts new file mode 100644 index 00000000..f1038dca --- /dev/null +++ b/Card/Event/Cancel.ts @@ -0,0 +1,13 @@ +import { isoly } from "isoly" +import { isly } from "isly" + +export interface Cancel { + type: "cancel" + at: isoly.DateTime +} +export namespace Cancel { + export const type = isly.object({ + type: isly.string(), + at: isly.fromIs("isoly.DateTime", isoly.DateTime.is), + }) +} diff --git a/Card/Event/Change.ts b/Card/Event/Change.ts new file mode 100644 index 00000000..6494f2ee --- /dev/null +++ b/Card/Event/Change.ts @@ -0,0 +1,18 @@ +import { isoly } from "isoly" +import { isly } from "isly" +import { Changeable } from "../Changeable" + +export interface Change { + type: "change" + at: isoly.DateTime + from: Changeable + to: Changeable +} +export namespace Change { + export const type = isly.object({ + type: isly.string("change"), + at: isly.fromIs("isoly.DateTime", isoly.DateTime.is), + from: Changeable.type, + to: Changeable.type, + }) +} diff --git a/Card/Event/Clearing.ts b/Card/Event/Clearing.ts new file mode 100644 index 00000000..5769e395 --- /dev/null +++ b/Card/Event/Clearing.ts @@ -0,0 +1,25 @@ +import { isoly } from "isoly" +import { isly } from "isly" + +export interface Clearing { + type: Clearing.Type + at: isoly.DateTime + currency: isoly.Currency + total: number + net: number + fee: number + charge?: number +} +export namespace Clearing { + export const types = ["capture", "refund"] as const + export type Type = typeof types[number] + export const type = isly.object({ + type: isly.string(types), + at: isly.fromIs("isoly.DateTime", isoly.DateTime.is), + currency: isly.string(), + total: isly.number(), + net: isly.number(), + fee: isly.number(), + charge: isly.number().optional(), + }) +} diff --git a/Card/Event/Create.ts b/Card/Event/Create.ts new file mode 100644 index 00000000..3e947d2c --- /dev/null +++ b/Card/Event/Create.ts @@ -0,0 +1,16 @@ +import { isoly } from "isoly" +import { isly } from "isly" +import { Amount } from "../../Amount" + +export interface Create { + type: "create" + at: isoly.DateTime + limit: Amount +} +export namespace Create { + export const type = isly.object({ + type: isly.string("create"), + at: isly.fromIs("isoly.DateTime", isoly.DateTime.is), + limit: Amount.type, + }) +} diff --git a/Card/Event/Index.spec.ts b/Card/Event/Index.spec.ts new file mode 100644 index 00000000..76b589cd --- /dev/null +++ b/Card/Event/Index.spec.ts @@ -0,0 +1,14 @@ +import { pax2pay } from "../../index" + +describe("Card.Event", () => { + const event: pax2pay.Card.Event = { + type: "authorization", + id: "19236hf", + outcome: "created", + created: "2023-12-13T12:11:00.000Z", + amount: ["UAH", 9999], + } + it("authorization", () => { + expect(pax2pay.Card.Event.type.is(event)).toBeTruthy() + }) +}) diff --git a/Card/Event/index.ts b/Card/Event/index.ts new file mode 100644 index 00000000..f2d7813b --- /dev/null +++ b/Card/Event/index.ts @@ -0,0 +1,43 @@ +import { isoly } from "isoly" +import { isly } from "isly" +import type { Authorization as ModelAuthorization } from "../../Authorization" +import type { Entry } from "../../Settlement/Entry" +import { Authorization as EventAuthorization } from "./Authorization" +import { Cancel as EventCancel } from "./Cancel" +import { Change as EventChange } from "./Change" +import { Clearing as EventClearing } from "./Clearing" +import { Create as EventCreate } from "./Create" + +export type Event = Event.Create | Event.Cancel | Event.Change | Event.Authorization | Event.Clearing + +export namespace Event { + export import Create = EventCreate + export import Cancel = EventCancel + export import Change = EventChange + export import Authorization = EventAuthorization + export import Clearing = EventClearing + export const type = isly.union(Create.type, Cancel.type, Change.type, Event.Authorization.type, Clearing.type) + export function fromAuthorization(authorization: ModelAuthorization): Event { + return { + type: "authorization", + id: authorization.id, + outcome: authorization.status != "approved" ? "rejected" : "created", + at: authorization.created, + reason: authorization.status == "approved" ? undefined : authorization.status, + amount: authorization.amount, // FIXME: we need the total transaction amount on auth + } + } + export function fromEntry(entry: Entry): Event | undefined { + return entry.type == "unknown" || entry.type == "cancel" + ? undefined + : { + type: entry.type, + at: isoly.DateTime.now(), + currency: entry.amount[0], + net: entry.amount[1], + fee: entry.fee.other[entry.amount[0]] ?? 0, + total: isoly.Currency.add(entry.amount[0], entry.amount[1], entry.fee.other[entry.amount[0]] ?? 0), // FIXME: this computation should probably be done in entry + // FIXME: charge: entry.charge, + } + } +} diff --git a/Card/Operation/Authorization.ts b/Card/Operation/Authorization.ts deleted file mode 100644 index 338d3e1d..00000000 --- a/Card/Operation/Authorization.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isoly } from "isoly" -import { isly } from "isly" -export interface Authorization { - type: "authorization" - id: string - status: Authorization.Status - reason?: string - created: isoly.DateTime -} - -export namespace Authorization { - export const statuses = ["created", "confirmed", "refunded", "captured", "cancelled"] as const - export type Status = typeof statuses[number] - export const type = isly.object({ - type: isly.string("authorization"), - id: isly.string(), - status: isly.string(statuses), - reason: isly.string().optional(), - created: isly.fromIs("isoly.DateTime", isoly.DateTime.is), - }) - export const is = type.is -} diff --git a/Card/Operation/Card.ts b/Card/Operation/Card.ts deleted file mode 100644 index f46d8dcc..00000000 --- a/Card/Operation/Card.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isoly } from "isoly" -import { isly } from "isly" -import { Changeable } from "../Changeable" - -export interface Card { - type: "card" - status: Card.Status - from?: Changeable - created: isoly.DateTime -} - -export namespace Card { - export const statuses = ["created", "changed", "cancelled"] as const - export type Status = typeof statuses[number] - export const type = isly.object({ - type: isly.string("card"), - status: isly.string(statuses), - from: Changeable.type.optional(), - created: isly.fromIs("isoly.DateTime", isoly.DateTime.is), - }) - export const is = type.is -} diff --git a/Card/Operation/Operation.spec.ts b/Card/Operation/Operation.spec.ts deleted file mode 100644 index 58fdf7f3..00000000 --- a/Card/Operation/Operation.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { pax2pay } from "../../index" - -describe("Card.Operation", () => { - const operation: pax2pay.Card.Operation = { - type: "authorization", - id: "19236hf", - status: pax2pay.Card.Operation.fromEntryStatus("capture"), - created: "2023-12-13T12:11:00.000Z", - } - it("authorization", () => { - expect(pax2pay.Card.Operation.is(operation)).toBeTruthy() - }) -}) diff --git a/Card/Operation/index.ts b/Card/Operation/index.ts deleted file mode 100644 index 1f11e3d6..00000000 --- a/Card/Operation/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { isoly } from "isoly" -import { isly } from "isly" -import { Authorization } from "../../Authorization" -import { Entry } from "../../Settlement/Entry" -import { Authorization as OperationAuthorization } from "./Authorization" -import { Card } from "./Card" - -export type Operation = Card | OperationAuthorization - -export namespace Operation { - export function fromAuthorization( - authorization: Authorization, - status: OperationAuthorization.Status - ): Operation | undefined { - return { - type: "authorization", - id: authorization?.id ?? authorization.transaction?.id ?? "unknown", - status, - created: isoly.DateTime.now(), - } - } - export function fromEntry(entry: Entry): Operation | undefined { - return entry.type == "unknown" - ? undefined - : { - type: "authorization", - id: (entry.type != "refund" ? entry.authorization?.id : entry.transaction?.id) ?? "unknown", - status: Operation.fromEntryStatus(entry.type), - created: isoly.DateTime.now(), - } - } - export function fromEntryStatus(status: Exclude): OperationAuthorization.Status { - const statusConverter: Record, OperationAuthorization.Status> = { - capture: "captured", - cancel: "cancelled", - refund: "refunded", - } - return statusConverter[status] - } - export const type = isly.union(Card.type, OperationAuthorization.type) - export const is = type.is -} diff --git a/Card/index.ts b/Card/index.ts index 004aba30..5799780f 100644 --- a/Card/index.ts +++ b/Card/index.ts @@ -7,9 +7,9 @@ import type { Rule } from "../Rule" import { type as ruleType } from "../Rule/type" import { Changeable as CardChangeable } from "./Changeable" import { Creatable as CardCreatable } from "./Creatable" +import { Event as CardEvent } from "./Event" import { Expiry as CardExpiry } from "./Expiry" import { Meta as CardMeta } from "./Meta" -import { Operation as CardOperation } from "./Operation" import { Preset as CardPreset } from "./Preset" import { Scheme as CardScheme } from "./Scheme" import { Stack as CardStack } from "./Stack" @@ -24,17 +24,12 @@ export interface Card { preset: CardPreset scheme: CardScheme reference?: string - details: { - iin: string - last4: string - expiry: CardExpiry - holder: string - token?: string - } + details: { iin: string; last4: string; expiry: CardExpiry; holder: string; token?: string } limit: Amount spent: Amount status: "active" | "cancelled" - history: CardOperation[] + history: any[] + events?: Card.Event[] rules: Rule[] meta?: CardMeta } @@ -59,7 +54,8 @@ export namespace Card { limit: isly.tuple(isly.fromIs("isoly.Currency", isoly.Currency.is), isly.number()), spent: isly.tuple(isly.fromIs("isoly.Currency", isoly.Currency.is), isly.number()), status: isly.union(isly.string("active"), isly.string("cancelled")), - history: isly.array(CardOperation.type), + history: isly.any(), + events: CardEvent.type.array().optional(), rules: ruleType.array(), meta: isly.fromIs("Card.Meta", CardMeta.is).optional(), }) @@ -69,13 +65,22 @@ export namespace Card { export import Meta = CardMeta export import Expiry = CardExpiry export import Changeable = CardChangeable - export import Operation = CardOperation + export import Event = CardEvent export import Scheme = CardScheme export import Stack = CardStack const csvMap: Record string | number | undefined> = { id: card => card.id, created: card => readableDate(card.created), - cancelled: card => readableDate(card.history.find(o => o.type == "card" && o.status == "cancelled")?.created), + cancelled: card => { + if (card.events) { + return readableDate(card.events.find(o => o.type == "cancel")?.at) + } else { + // for legacy reasons + return readableDate( + card.history?.find(o => o.type == "card" && "status" in o && o.status == "cancelled")?.created + ) + } + }, "organization.code": card => card.organization, realm: card => card.realm, account: card => card.account, @@ -89,8 +94,18 @@ export namespace Card { expiry: card => readableDate(Expiry.toDateTime(card.details.expiry)), iin: card => card.details.iin, holder: card => card.details.holder, - "authorization.count": card => card.history.filter(o => o.type == "authorization" && o.status == "created").length, - "capture.count": card => card.history.filter(o => o.type == "authorization" && o.status == "captured").length, + "authorization.count": card => { + const authorizations = card.events?.filter(o => o.type == "authorization" && o.outcome == "created").length ?? 0 + const legacy = + card.history?.filter(o => o.type == "authorization" && "status" in o && o.status == "created").length ?? 0 + return authorizations + legacy + }, + "capture.count": card => { + const captures = card.events?.filter(o => o.type == "capture").length ?? 0 + const legacy = + card.history?.filter(o => o.type == "authorization" && "status" in o && o.status == "captured").length ?? 0 + return captures + legacy + }, } function readableDate(date: isoly.DateTime | undefined): string | undefined { return date && date.slice(0, 10) + " " + (date.endsWith("Z") ? date.slice(11, -1) : date.slice(11))