@phis/contracts 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,13 @@ The agreements phis (`@phis/server`) keeps with what surrounds it: the site UI (
4
4
  separately shipped Add-ons. Each lives under its own subpath, because they hold between different parties
5
5
  and change at different moments.
6
6
 
7
+ > **Part of phis.** This package is one piece of a larger whole and does nothing on its own. A Phi
8
+ > installation is the server [`@phis/server`](https://github.com/phis-dev/phis-server), installed as the
9
+ > Debian package `phis` from `apt.phisys.com` (not from npm), plus Sites built on
10
+ > [`@phis/ui`](https://github.com/phis-dev/phis-ui), which brings this package along. Installation and
11
+ > documentation: [phis.dev](https://phis.dev). Install it directly only to build an Add-on or a Module
12
+ > against these contracts.
13
+
7
14
  ```
8
15
  @phis/contracts/addon what phis and a separately shipped Add-on promise each other
9
16
  @phis/contracts/payments the approved Payment consumer, Provider and merchant-profile types
package/dist/addon.d.ts CHANGED
@@ -199,6 +199,46 @@ export type PhisAddonEventHandlerDescriptor = {
199
199
  event: PhisAddonEventId;
200
200
  handler: string;
201
201
  };
202
+ /**
203
+ * One sentence of an occasion, with the parameters it takes in order.
204
+ *
205
+ * `%1`, `%2`, ... in `text` stand for the named parameters, the way Core's own templates are written,
206
+ * so the sentence translates as one unit and the values are put in afterwards. A text parameter is
207
+ * escaped where it lands; a path parameter arrives as a link on the Site's own public address.
208
+ */
209
+ export type PhisAddonOccasionLine = {
210
+ text: string;
211
+ parameters?: string[];
212
+ };
213
+ /**
214
+ * One kind of message this Add-on may ask Core to write to a person.
215
+ *
216
+ * Declared rather than composed at the call: the words are part of the manifest and therefore of the
217
+ * digest, so what a Site can say in an Add-on's name is readable at install and cannot change under a
218
+ * running Site. Core renders the lines through its own layout, salutation and translation, and adds
219
+ * what the class owes -- which is why an Add-on cannot produce a mail without Core's frame.
220
+ *
221
+ * `class` is `asked-for` and nothing else this release: the person's own action is the reason for the
222
+ * message (an order they placed, a request they made), no consent bit applies and no unsubscribe link
223
+ * is offered. The sent class -- a message somebody decided to send to people who did not ask today --
224
+ * is Core's own bulk case and is not declarable here until it is.
225
+ */
226
+ export type PhisAddonOccasionDescriptor = {
227
+ /** `^[a-z][a-z0-9-]{0,63}$`; the name a `notify` call and the outbox row carry. */
228
+ name: string;
229
+ class: "asked-for";
230
+ description?: string;
231
+ subject: PhisAddonOccasionLine;
232
+ lines: PhisAddonOccasionLine[];
233
+ /**
234
+ * What a call must bring, by name. `text` is inserted as written; `path` is a Site-relative path
235
+ * (`/shop/order/1234`) that Core turns into an absolute link on the Site's public address.
236
+ */
237
+ parameters: {
238
+ name: string;
239
+ type: "text" | "path";
240
+ }[];
241
+ };
202
242
  export type PhisAddonManifestV1 = {
203
243
  manifestVersion: typeof PHIS_ADDON_MANIFEST_VERSION;
204
244
  addonId: string;
@@ -260,6 +300,13 @@ export type PhisAddonManifestV1 = {
260
300
  * provider, and every write here refuses a group carrying anybody else's.
261
301
  */
262
302
  groupWriter?: PhisAddonGroupWriterDescriptor;
303
+ /**
304
+ * The occasions this Add-on may write to a person about. Absent means none, and `notify` then has
305
+ * nothing it could be asked for.
306
+ *
307
+ * Optional for the reason `settings` is: it arrived after the manifest version was in the wild.
308
+ */
309
+ occasions?: PhisAddonOccasionDescriptor[];
263
310
  /**
264
311
  * The Modules this package's other half contributes. Absent means it has no Module half.
265
312
  *
@@ -107,6 +107,8 @@ export type PhisAddonCapabilities = {
107
107
  payments?: PhisPaymentsCapabilityV1;
108
108
  /** Supplied only to an original, bound Payment Provider hook request. */
109
109
  paymentNotifications?: PhisPaymentNotificationsCapabilityV1;
110
+ /** Writing to a person about an occasion this Add-on declared; Core owns the frame and the sending. */
111
+ notifications?: PhisNotificationsCapabilityV1;
110
112
  };
111
113
  /**
112
114
  * The roles capability: `@phis/server/roles:v1`.
@@ -224,6 +226,55 @@ export type PhisSecretsCapabilityV1 = {
224
226
  };
225
227
  /** Which secret `PhisSecretsCapabilityV1.read` names: the Site's own, or the one shared by all Sites. */
226
228
  export type PhisSecretScope = "site" | "addon";
229
+ /**
230
+ * The notifications capability: `@phis/server/notifications:v1`.
231
+ *
232
+ * `notify` names **who** and **what happened**, and nothing about how. The occasion is one this Add-on
233
+ * declared in its manifest (`occasions`), so its words are sealed by the digest; Core resolves the
234
+ * address, picks the language, writes the salutation, renders the lines through its own layout and
235
+ * translation, chooses the sender, and sends -- through the outbox, with retry and a record. There is no
236
+ * subject, body, `from`, header or attachment on the call, because every one of those is a decision
237
+ * about how a Site speaks, and a Site speaks with one voice.
238
+ *
239
+ * Two recipients. A `user` is a member of this Site, named by id: the address is resolved when the
240
+ * message is sent, so a corrected address is used and a membership that is gone is not written to.
241
+ * A `contact` is a person who has no account here and gave this Add-on their address for exactly this
242
+ * purpose -- a guest who placed an order, somebody who filled in a form. It is the one place an Add-on
243
+ * hands Core an address, and it is admitted on two conditions Core enforces: the occasion is one the
244
+ * person's own action asked for (every declared occasion is, this release), and the locale is stated,
245
+ * because there is no profile to ask.
246
+ *
247
+ * `params` are values about the event, never about the person: a `text` parameter is inserted as
248
+ * written, a `path` parameter is a Site-relative path Core turns into an absolute link on the Site's own
249
+ * public address. An occasion cannot put an arbitrary URL into a Site's outgoing mail.
250
+ *
251
+ * Answers the outbox row's id, or `null` when `dedupeKey` named a message already written -- which is
252
+ * what makes a repeated run safe. Refused outright: an undeclared occasion, a parameter the occasion
253
+ * did not declare or lacks, a `user` who is not a member of this Site, and any call from an
254
+ * unverified hook, which may change nothing.
255
+ */
256
+ export type PhisNotificationsCapabilityV1 = {
257
+ notify(input: PhisNotificationInput): Promise<{
258
+ notificationId: string | null;
259
+ }>;
260
+ };
261
+ export type PhisNotificationRecipient = {
262
+ kind: "user";
263
+ userId: number;
264
+ locale?: string | null;
265
+ } | {
266
+ kind: "contact";
267
+ address: string;
268
+ locale: string;
269
+ name?: string | null;
270
+ };
271
+ export type PhisNotificationInput = {
272
+ occasion: string;
273
+ recipient: PhisNotificationRecipient;
274
+ params: Readonly<Record<string, string>>;
275
+ /** Unique within the Site; a second call with the same key writes nothing and answers `null`. */
276
+ dedupeKey?: string | null;
277
+ };
227
278
  /**
228
279
  * What an Add-on may learn about a person.
229
280
  *
package/dist/core.d.ts CHANGED
@@ -48,6 +48,8 @@ export declare const PHIS_CORE_CAPABILITIES: {
48
48
  readonly roles: "@phis/server/roles:v1";
49
49
  readonly payments: "@phis/server/payments:v1";
50
50
  readonly paymentNotifications: "@phis/server/payment-notifications:v1";
51
+ /** Writing to a person about an occasion the Add-on declared, with Core owning frame and transport. */
52
+ readonly notifications: "@phis/server/notifications:v1";
51
53
  };
52
54
  export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof PHIS_CORE_CAPABILITIES];
53
55
  /**
package/dist/core.js CHANGED
@@ -47,6 +47,8 @@ export const PHIS_CORE_CAPABILITIES = {
47
47
  roles: "@phis/server/roles:v1",
48
48
  payments: "@phis/server/payments:v1",
49
49
  paymentNotifications: "@phis/server/payment-notifications:v1",
50
+ /** Writing to a person about an occasion the Add-on declared, with Core owning frame and transport. */
51
+ notifications: "@phis/server/notifications:v1",
50
52
  };
51
53
  /**
52
54
  * What Core tells an Add-on about after the fact.
@@ -4,6 +4,10 @@
4
4
  * Execution belongs to Core; its implemented capability, service and event identities live in core.ts.
5
5
  * These declarations contain no SDK, credentials, persistence or exchange-rate conversion. See phis-server's
6
6
  * PAYMENTS.md and PAYMENTS_STRIPE.md for authority, durability and Provider mapping requirements.
7
+ *
8
+ * `amount` is always the consumer's base amount. Under a pricing policy other than `final`/`none` the
9
+ * Provider adds tax, shipping and discounts on top of it and reports the customer's total with its parts
10
+ * (PAYMENTS.md section 12); authorized, captured and refunded funds are measured against that total.
7
11
  */
8
12
  export type PhisPaymentId = string;
9
13
  export type PhisPaymentOperationId = string;
@@ -23,6 +27,7 @@ export type PhisPaymentFeatures = {
23
27
  canCancel: boolean;
24
28
  canRefund: boolean;
25
29
  canPartialRefund: boolean;
30
+ pricing: PhisPaymentPricingFeatures;
26
31
  };
27
32
  export type PhisPaymentMethod = {
28
33
  profileKey: string;
@@ -47,7 +52,10 @@ export type PhisPayment = {
47
52
  reference: string;
48
53
  profileKey: string;
49
54
  mode: "test" | "live";
55
+ /** The consumer's base amount; the customer pays `totals.total` once the Provider reports it. */
50
56
  amount: PhisPaymentMoney;
57
+ pricing: PhisPaymentPricing;
58
+ totals: PhisPaymentTotals | null;
51
59
  captureMode: PhisPaymentCaptureMode;
52
60
  status: PhisPaymentStatus;
53
61
  authorizedMinor: string;
@@ -71,6 +79,8 @@ export type PhisPaymentRefund = {
71
79
  id: PhisPaymentRefundId;
72
80
  paymentId: PhisPaymentId;
73
81
  amount: PhisPaymentMoney;
82
+ /** The tax or discount share of this refund when the Provider reports one; informational. */
83
+ additions: readonly PhisPaymentAddition[] | null;
74
84
  status: "pending" | "succeeded" | "failed" | "canceled";
75
85
  };
76
86
  export type PhisPaymentCommandResult = {
@@ -83,6 +93,7 @@ export type PhisPaymentCreateInput = {
83
93
  reference: string;
84
94
  idempotencyKey: string;
85
95
  amount: PhisPaymentMoney;
96
+ pricing: PhisPaymentPricing;
86
97
  description: string;
87
98
  captureMode: PhisPaymentCaptureMode;
88
99
  methodKey: string | null;
@@ -99,6 +110,7 @@ export interface PhisPaymentsCapabilityV1 {
99
110
  amount: PhisPaymentMoney;
100
111
  captureMode: PhisPaymentCaptureMode;
101
112
  checkoutKind: PhisPaymentCheckoutKind;
113
+ pricing: PhisPaymentPricingSelector;
102
114
  }): Promise<readonly PhisPaymentMethod[]>;
103
115
  /** Core binds Site/consumer/profile; the consumer checks access to its business resource. */
104
116
  create(input: PhisPaymentCreateInput): Promise<PhisPaymentCommandResult>;
@@ -143,7 +155,10 @@ export type PhisPaymentProviderObservation = {
143
155
  merchantIdentity: string;
144
156
  mode: "test" | "live";
145
157
  status: PhisPaymentStatus;
158
+ /** The base amount as created; a Provider never changes it. */
146
159
  amount: PhisPaymentMoney;
160
+ /** Null only while the Provider cannot yet know the total; required from `authorized` on. */
161
+ totals: PhisPaymentObservedTotals | null;
147
162
  authorizedMinor: string;
148
163
  capturedMinor: string;
149
164
  authorizationExpiresAt: string | null;
@@ -154,6 +169,7 @@ export type PhisPaymentProviderObservation = {
154
169
  binding: PhisPaymentProviderBinding;
155
170
  minor: string;
156
171
  status: PhisPaymentRefund["status"];
172
+ additions: readonly PhisPaymentAddition[] | null;
157
173
  }[];
158
174
  };
159
175
  export type PhisPaymentProviderCommand = {
@@ -164,6 +180,7 @@ export type PhisPaymentProviderCommand = {
164
180
  } & ({
165
181
  action: "create";
166
182
  amount: PhisPaymentMoney;
183
+ pricing: PhisPaymentPricing;
167
184
  description: string;
168
185
  captureMode: PhisPaymentCaptureMode;
169
186
  methodKey: string | null;
@@ -211,6 +228,7 @@ export interface PhisPaymentProviderV1 {
211
228
  amount: PhisPaymentMoney;
212
229
  captureMode: PhisPaymentCaptureMode;
213
230
  checkoutKind: PhisPaymentCheckoutKind;
231
+ pricing: PhisPaymentPricingSelector;
214
232
  signal: AbortSignal;
215
233
  }): Promise<PhisPaymentProviderProbe>;
216
234
  execute(input: PhisPaymentProviderCommand): Promise<PhisPaymentProviderExecution>;
@@ -272,3 +290,96 @@ export type PhisPaymentChangedEventFacts = {
272
290
  refundId: PhisPaymentRefundId | null;
273
291
  };
274
292
  };
293
+ export type PhisPaymentPricingBasis = "final" | "net";
294
+ export type PhisPaymentTaxHandling = "none" | "provider" | "merchant-of-record";
295
+ export type PhisPaymentPromotionCodes = "none" | "provider";
296
+ /** What is sold, never a rate; each Provider maps the closed vocabulary to its own codes. */
297
+ export type PhisPaymentTaxCategory = "general" | "reduced-goods" | "digital-goods" | "digital-service" | "service" | "shipping" | "exempt";
298
+ export type PhisPaymentLineItem = {
299
+ reference: string;
300
+ description: string;
301
+ quantity: number;
302
+ unitMinor: string;
303
+ taxCategory: PhisPaymentTaxCategory;
304
+ /** Provider-specific code overriding the category mapping, keyed by provider key. */
305
+ providerTaxCodes: Readonly<Record<string, string>> | null;
306
+ };
307
+ /** A reduction the consumer decided; the Provider shows it and taxes the reduced amount. */
308
+ export type PhisPaymentDiscount = {
309
+ reference: string;
310
+ label: string;
311
+ minor: string;
312
+ };
313
+ export type PhisPaymentCustomerDetails = {
314
+ country: string;
315
+ postalCode: string | null;
316
+ region: string | null;
317
+ addressSource: "billing" | "shipping";
318
+ taxIds: readonly {
319
+ type: string;
320
+ value: string;
321
+ }[];
322
+ };
323
+ /**
324
+ * `final`/`none` is v1: the base amount is what the customer pays and the Provider adds nothing. Every
325
+ * other policy carries line items; the base amount then equals their sum plus shipping minus discounts.
326
+ */
327
+ export type PhisPaymentPricing = {
328
+ basis: "final";
329
+ tax: "none";
330
+ } | {
331
+ basis: PhisPaymentPricingBasis;
332
+ tax: PhisPaymentTaxHandling;
333
+ lineItems: readonly PhisPaymentLineItem[];
334
+ shipping: {
335
+ minor: string;
336
+ taxCategory: "shipping";
337
+ } | null;
338
+ discounts: readonly PhisPaymentDiscount[];
339
+ promotionCodes: PhisPaymentPromotionCodes;
340
+ customer: PhisPaymentCustomerDetails | null;
341
+ };
342
+ /** The facts of a policy a method must support; `discounts` is the number of stated discounts. */
343
+ export type PhisPaymentPricingSelector = {
344
+ basis: PhisPaymentPricingBasis;
345
+ tax: PhisPaymentTaxHandling;
346
+ discounts: number;
347
+ promotionCodes: PhisPaymentPromotionCodes;
348
+ };
349
+ export type PhisPaymentPricingFeatures = {
350
+ /** Always includes `final`. */
351
+ bases: readonly PhisPaymentPricingBasis[];
352
+ /** Always includes `none`. */
353
+ tax: readonly PhisPaymentTaxHandling[];
354
+ /** How many stated discounts one payment may carry; 0 when the Provider shows none. */
355
+ maxDiscounts: number;
356
+ /** Whether a customer may enter a code on the Provider's page, and whether next to stated discounts. */
357
+ promotionCodes: "none" | "alone" | "with-discounts";
358
+ collectsCustomerDetails: boolean;
359
+ collectsTaxIds: boolean;
360
+ };
361
+ export type PhisPaymentAdditionKind = "tax" | "shipping" | "discount";
362
+ export type PhisPaymentAddition = {
363
+ kind: PhisPaymentAdditionKind;
364
+ minor: string;
365
+ label: string;
366
+ /** Contained in the base amount rather than added to it; a `discount` is never inclusive. */
367
+ inclusive: boolean;
368
+ /** Display-only decimal string such as "8.1"; never enters Core arithmetic. */
369
+ rate: string | null;
370
+ /** ISO country code, optionally followed by "-" and a region. */
371
+ jurisdiction: string | null;
372
+ /** Who owes a tax addition to the authority: the merchant, or the Provider as Merchant of Record. */
373
+ collectedBy: "merchant" | "provider";
374
+ /** The promotion code or stated discount reference behind a `discount`; null for every other kind. */
375
+ code: string | null;
376
+ };
377
+ export type PhisPaymentObservedTotals = {
378
+ total: PhisPaymentMoney;
379
+ additions: readonly PhisPaymentAddition[];
380
+ customer: PhisPaymentCustomerDetails | null;
381
+ };
382
+ export type PhisPaymentTotals = PhisPaymentObservedTotals & {
383
+ /** Set by Core when the total became binding; a differing total afterwards is an inconsistency. */
384
+ frozenAt: string | null;
385
+ };
package/dist/payments.js CHANGED
@@ -4,5 +4,9 @@
4
4
  * Execution belongs to Core; its implemented capability, service and event identities live in core.ts.
5
5
  * These declarations contain no SDK, credentials, persistence or exchange-rate conversion. See phis-server's
6
6
  * PAYMENTS.md and PAYMENTS_STRIPE.md for authority, durability and Provider mapping requirements.
7
+ *
8
+ * `amount` is always the consumer's base amount. Under a pricing policy other than `final`/`none` the
9
+ * Provider adds tax, shipping and discounts on top of it and reports the customer's total with its parts
10
+ * (PAYMENTS.md section 12); authorized, captured and refunded funds are measured against that total.
7
11
  */
8
12
  export {};
package/dist/queries.d.ts CHANGED
@@ -84,6 +84,20 @@ export type PhisAddonQueryCondition = {
84
84
  column: string;
85
85
  negated?: boolean;
86
86
  }
87
+ /**
88
+ * Whether bits of a flags column are set: every bit of `mask` (`all`), or none of them (`none`).
89
+ *
90
+ * Yes/no state is kept as bit flags rather than boolean columns, so "the rows that are not a draft"
91
+ * is a question about one bit of an integer. `compare` cannot ask it -- `=` sees the whole number --
92
+ * and without this an Add-on reads every row and filters in code, which also breaks paging: a page of
93
+ * 25 comes back with fewer. The mask is a positive integer; the column is an integer column.
94
+ */
95
+ | {
96
+ kind: "flags";
97
+ column: string;
98
+ mask: PhisAddonQueryValue;
99
+ match: "all" | "none";
100
+ }
87
101
  /**
88
102
  * Whether a text column contains this fragment, case-insensitively.
89
103
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phis/contracts",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -140,7 +140,7 @@
140
140
  "type": "git",
141
141
  "url": "git+https://github.com/phis-dev/phis-contracts.git"
142
142
  },
143
- "homepage": "https://github.com/phis-dev/phis-contracts#readme",
143
+ "homepage": "https://phis.dev",
144
144
  "bugs": {
145
145
  "url": "https://github.com/phis-dev/phis-contracts/issues"
146
146
  }