@engine9/schemas 1.9.0

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.
Files changed (122) hide show
  1. package/.eslintignore +1 -0
  2. package/.prettierrc +7 -0
  3. package/AGENTS.md +10 -0
  4. package/LICENSE +21 -0
  5. package/README.md +33 -0
  6. package/channels/email/README.md +83 -0
  7. package/channels/email/index.js +22 -0
  8. package/channels/email/search.js +86 -0
  9. package/channels/email/segments.js +81 -0
  10. package/eslint.config.js +21 -0
  11. package/event/README.md +79 -0
  12. package/event/index.js +17 -0
  13. package/event/schema.js +111 -0
  14. package/event/search.js +51 -0
  15. package/message/index.js +8 -0
  16. package/message/schema.js +130 -0
  17. package/message/ui.console.json5 +233 -0
  18. package/package.json +26 -0
  19. package/person/index.js +100 -0
  20. package/person/metrics.js +17 -0
  21. package/person/schema.js +47 -0
  22. package/person/transforms/inbound/normalize_field_names.js +16 -0
  23. package/person/transforms/inbound/upsert_tables.js +21 -0
  24. package/person/transforms/outbound/appendName.js +32 -0
  25. package/person/transforms/simpleMap.js +34 -0
  26. package/person/ui.console.json5 +165 -0
  27. package/person/upsert.test.js +72 -0
  28. package/person_address/index.js +30 -0
  29. package/person_address/normalize.js +232 -0
  30. package/person_address/normalize.test.js +69 -0
  31. package/person_address/schema.js +49 -0
  32. package/person_address/transforms/inbound/normalize.js +23 -0
  33. package/person_address/transforms/inbound/upsert_tables.js +51 -0
  34. package/person_address/transforms/outbound/appendAddress.js +87 -0
  35. package/person_address/ui.console.json5 +31 -0
  36. package/person_custom/index.js +24 -0
  37. package/person_custom/transforms/appendRemoteId.js +31 -0
  38. package/person_custom/transforms/inbound/upsert_tables.js +31 -0
  39. package/person_email/README.md +82 -0
  40. package/person_email/index.js +35 -0
  41. package/person_email/schema.js +49 -0
  42. package/person_email/search.js +61 -0
  43. package/person_email/segments.js +16 -0
  44. package/person_email/transforms/inbound/extract_identifiers.js +33 -0
  45. package/person_email/transforms/inbound/upsert_tables.js +142 -0
  46. package/person_email/transforms/outbound/appendEmail.js +32 -0
  47. package/person_email/transforms/outbound/appendEmailHash.js +20 -0
  48. package/person_email/transforms/outbound/tables.js +14 -0
  49. package/person_email/ui.console.json5 +58 -0
  50. package/person_email/upsert.test.js +126 -0
  51. package/person_hash/README.md +70 -0
  52. package/person_hash/hash.js +103 -0
  53. package/person_hash/hash.test.js +165 -0
  54. package/person_hash/index.js +37 -0
  55. package/person_hash/schema.js +54 -0
  56. package/person_hash/search.js +86 -0
  57. package/person_hash/transforms/inbound/extract_identifiers.js +70 -0
  58. package/person_hash/transforms/inbound/upsert_tables.js +105 -0
  59. package/person_hash/transforms/outbound/appendEmailHash.js +42 -0
  60. package/person_hash/transforms/outbound/appendPhoneHash.js +42 -0
  61. package/person_hash/ui.console.json5 +80 -0
  62. package/person_phone/README.md +79 -0
  63. package/person_phone/index.js +33 -0
  64. package/person_phone/schema.js +58 -0
  65. package/person_phone/search.js +79 -0
  66. package/person_phone/segments.js +26 -0
  67. package/person_phone/transforms/inbound/extract_identifiers.js +59 -0
  68. package/person_phone/transforms/inbound/upsert_tables.js +119 -0
  69. package/person_phone/transforms/outbound/appendPhoneHash.js +37 -0
  70. package/person_phone/ui.console.json5 +31 -0
  71. package/person_phone/upsert.test.js +50 -0
  72. package/person_remote/index.js +58 -0
  73. package/person_remote/metrics.js +17 -0
  74. package/person_remote/schema.js +21 -0
  75. package/person_remote/transforms/inbound/extract_identifiers.js +34 -0
  76. package/person_remote/transforms/inbound/upsert_tables.js +92 -0
  77. package/person_remote/transforms/outbound/appendRemotePersonId.js +59 -0
  78. package/plugin/index.js +13 -0
  79. package/plugin/schema.js +70 -0
  80. package/plugin/settings.js +19 -0
  81. package/plugin/ui.console.json5 +79 -0
  82. package/report/index.js +7 -0
  83. package/report/ui.console.json5 +21 -0
  84. package/segment/index.js +21 -0
  85. package/segment/schema.js +81 -0
  86. package/segment/search.js +64 -0
  87. package/segment/transforms/inbound/upsert.js +44 -0
  88. package/segment/ui.console.json5 +142 -0
  89. package/segment_stats/index.js +16 -0
  90. package/segment_stats/schema.js +30 -0
  91. package/source_code/index.js +16 -0
  92. package/source_code/metrics.js +28 -0
  93. package/source_code/schema.js +29 -0
  94. package/source_code/ui.console.json5 +31 -0
  95. package/stacks/limited-pii/index.js +8 -0
  96. package/stacks/limited-pii/stack.json +22 -0
  97. package/stacks/stacks.test.js +27 -0
  98. package/stacks/standard/index.js +7 -0
  99. package/stacks/standard/stack.json +20 -0
  100. package/tag/index.js +13 -0
  101. package/tag/schema.js +23 -0
  102. package/task/index.js +13 -0
  103. package/task/schema.js +54 -0
  104. package/task/ui.console.json5 +60 -0
  105. package/timeline/index.js +18 -0
  106. package/timeline/schema.js +18 -0
  107. package/timeline/transforms/inbound/upsert_tables.js +22 -0
  108. package/timeline/ui.console.json5 +31 -0
  109. package/transaction/contact_details/index.js +11 -0
  110. package/transaction/contact_details/schema.js +22 -0
  111. package/transaction/core/README.md +79 -0
  112. package/transaction/core/attribute.test.js +231 -0
  113. package/transaction/core/index.js +31 -0
  114. package/transaction/core/metrics.js +20 -0
  115. package/transaction/core/schema.js +45 -0
  116. package/transaction/core/search.js +87 -0
  117. package/transaction/core/segments.js +14 -0
  118. package/transaction/core/transforms/appendTransactionSummary.js +101 -0
  119. package/transaction/core/transforms/attribute.js +327 -0
  120. package/transaction/core/transforms/inbound/upsert_tables.js +57 -0
  121. package/utilities/limited-pii/index.js +14 -0
  122. package/utilities/limited-pii/settings.js +12 -0
@@ -0,0 +1,31 @@
1
+ {
2
+ routes: {
3
+ '/person/:id/*': {
4
+ components: {
5
+ sidebar: {
6
+ items: [
7
+ {
8
+ component: 'Link',
9
+ properties: {
10
+ label: 'Timeline',
11
+ to: 'timeline'
12
+ }
13
+ }
14
+ ]
15
+ },
16
+ main: [
17
+ {
18
+ path: 'timeline',
19
+ label: 'Timeline',
20
+ component: 'RecordDisplay',
21
+ properties: {
22
+ table: 'timeline',
23
+ conditions: [{ eql: 'person_id={{person_id}}' }],
24
+ rows: ['{{email}}']
25
+ }
26
+ }
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ import stack from './stack.json' with { type: 'json' };
2
+
3
+ const metadata = { ...stack };
4
+ const include = stack.include;
5
+ const exclude = stack.exclude;
6
+
7
+ export { metadata, include, exclude };
8
+ export default { metadata };
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@engine9/schemas/stacks/limited-pii",
3
+ "description": "PII-minimized Engine9 stack: hash identifiers instead of plaintext email, phone, or postal address. Excludes person_email, person_phone, and person_address even if requested.",
4
+ "version": "1.1.0",
5
+ "unique": true,
6
+ "include": [
7
+ "@engine9/schemas/plugin",
8
+ "@engine9/schemas/person",
9
+ "@engine9/schemas/person_remote",
10
+ "@engine9/schemas/segment",
11
+ "@engine9/schemas/timeline",
12
+ "@engine9/schemas/source_code",
13
+ "@engine9/schemas/transaction/core",
14
+ "@engine9/schemas/transaction/contact_details",
15
+ "@engine9/schemas/person_hash"
16
+ ],
17
+ "exclude": [
18
+ "@engine9/schemas/person_email",
19
+ "@engine9/schemas/person_phone",
20
+ "@engine9/schemas/person_address"
21
+ ]
22
+ }
@@ -0,0 +1,27 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { metadata as standard } from './standard/index.js';
4
+ import { metadata as limitedPii } from './limited-pii/index.js';
5
+
6
+ test('standard stack includes plaintext contact plugins and does not exclude them', () => {
7
+ assert.equal(standard.name, '@engine9/schemas/stacks/standard');
8
+ assert.ok(standard.include.includes('@engine9/schemas/person_email'));
9
+ assert.ok(standard.include.includes('@engine9/schemas/person_phone'));
10
+ assert.ok(standard.include.includes('@engine9/schemas/person_address'));
11
+ assert.ok(standard.include.includes('@engine9/schemas/timeline'));
12
+ assert.ok(standard.include.includes('@engine9/schemas/source_code'));
13
+ assert.ok(standard.include.includes('@engine9/schemas/transaction/core'));
14
+ assert.ok(!standard.include.includes('@engine9/plugins/reports/people'));
15
+ assert.deepEqual(standard.exclude, []);
16
+ });
17
+
18
+ test('limited-pii stack includes person_hash and excludes plaintext contact plugins', () => {
19
+ assert.equal(limitedPii.name, '@engine9/schemas/stacks/limited-pii');
20
+ assert.ok(limitedPii.include.includes('@engine9/schemas/person_hash'));
21
+ assert.ok(!limitedPii.include.includes('@engine9/schemas/person_email'));
22
+ assert.ok(!limitedPii.include.includes('@engine9/schemas/person_phone'));
23
+ assert.ok(!limitedPii.include.includes('@engine9/schemas/person_address'));
24
+ assert.ok(limitedPii.exclude.includes('@engine9/schemas/person_email'));
25
+ assert.ok(limitedPii.exclude.includes('@engine9/schemas/person_phone'));
26
+ assert.ok(limitedPii.exclude.includes('@engine9/schemas/person_address'));
27
+ });
@@ -0,0 +1,7 @@
1
+ import stack from './stack.json' with { type: 'json' };
2
+
3
+ const metadata = { ...stack };
4
+ const include = stack.include;
5
+
6
+ export { metadata, include };
7
+ export default { metadata };
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@engine9/schemas/stacks/standard",
3
+ "description": "Opt-in Engine9 schema stack: person, plaintext email, phone, and postal address, timeline, source codes, and transactions. Reports install separately.",
4
+ "version": "1.0.0",
5
+ "unique": true,
6
+ "include": [
7
+ "@engine9/schemas/plugin",
8
+ "@engine9/schemas/person",
9
+ "@engine9/schemas/person_remote",
10
+ "@engine9/schemas/segment",
11
+ "@engine9/schemas/person_email",
12
+ "@engine9/schemas/person_phone",
13
+ "@engine9/schemas/person_address",
14
+ "@engine9/schemas/timeline",
15
+ "@engine9/schemas/source_code",
16
+ "@engine9/schemas/transaction/core",
17
+ "@engine9/schemas/transaction/contact_details"
18
+ ],
19
+ "exclude": []
20
+ }
package/tag/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import schema from './schema.js';
2
+ const metadata = {
3
+ name: '@engine9/schemas/tag',
4
+ dependencies: {
5
+ '@engine9/schemas/person': '>=1.9.0'
6
+ }
7
+ };
8
+ export { metadata };
9
+ export { schema };
10
+ export default {
11
+ metadata,
12
+ schema
13
+ };
package/tag/schema.js ADDED
@@ -0,0 +1,23 @@
1
+ export const tables = [
2
+ {
3
+ name: 'tag',
4
+ columns: {
5
+ id: 'uuid',
6
+ table_name: 'string',
7
+ tag_category: 'string',
8
+ tag: 'string'
9
+ }
10
+ },
11
+ {
12
+ name: 'tag_row',
13
+ columns: {
14
+ id: 'id',
15
+ tag_id: 'foreign_uuid',
16
+ row_id: 'id',
17
+ modified_at: 'modified_at'
18
+ }
19
+ }
20
+ ];
21
+ export default {
22
+ tables
23
+ };
package/task/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import schema from './schema.js';
2
+
3
+ const metadata = {
4
+ name: '@engine9/schemas/task',
5
+ schemas: ['schema.js']
6
+ };
7
+
8
+ export { metadata };
9
+ export { schema };
10
+ export default {
11
+ metadata,
12
+ schema
13
+ };
package/task/schema.js ADDED
@@ -0,0 +1,54 @@
1
+ export const tables = [
2
+ {
3
+ name: 'task_run',
4
+ columns: {
5
+ id: 'id_uuid',
6
+ account_id: 'string',
7
+ originator: { type: 'string', nullable: false, default_value: 'sql' },
8
+ flow_run_id: 'id_uuid',
9
+ task_key: 'string',
10
+ dynamic_key: { type: 'string', nullable: false, default_value: '0' },
11
+ name: 'string',
12
+ cache_key: 'string',
13
+ cache_expiration: 'datetime',
14
+ task_version: 'string',
15
+ empirical_policy: 'json',
16
+ tags: 'json',
17
+ labels: 'json',
18
+ state_id: 'id_uuid',
19
+ state_type: {
20
+ type: 'enum',
21
+ nullable: false,
22
+ default_value: 'PENDING',
23
+ values: ['PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED', 'CRASHED']
24
+ },
25
+ state_name: 'string',
26
+ state: 'json',
27
+ task_inputs: 'json',
28
+ run_count: 'int',
29
+ flow_run_run_count: 'int',
30
+ expected_start_time: 'datetime',
31
+ next_scheduled_start_time: 'datetime',
32
+ start_time: 'datetime',
33
+ end_time: 'datetime',
34
+ total_run_time: 'double',
35
+ estimated_run_time: 'double',
36
+ estimated_start_time_delta: 'double',
37
+ created_at: 'created_at',
38
+ modified_at: 'modified_at'
39
+ },
40
+ indexes: [
41
+ { columns: 'id', primary: true },
42
+ { columns: ['account_id'] },
43
+ { columns: ['originator'] },
44
+ { columns: ['flow_run_id'] },
45
+ { columns: ['state_type'] },
46
+ { columns: ['task_key'] },
47
+ { columns: ['created_at'] }
48
+ ]
49
+ }
50
+ ];
51
+
52
+ export default {
53
+ tables
54
+ };
@@ -0,0 +1,60 @@
1
+ {
2
+ menu: {
3
+ 'data-menu': {
4
+ title: 'Data',
5
+ icon: 'data',
6
+ type: 'collapse',
7
+ children: {
8
+ task: {
9
+ title: 'Tasks',
10
+ icon: 'forklift',
11
+ url: '/task'
12
+ }
13
+ }
14
+ }
15
+ },
16
+ routes: {
17
+ task: {
18
+ layout: 'full_width',
19
+ components: {
20
+ header: ['Tasks'],
21
+ main: [
22
+ {
23
+ component: 'RecordTable',
24
+ properties: {
25
+ table: 'task_run',
26
+ columns: [
27
+ { title: 'Id', dataIndex: 'id', sorter: true, width: '24%' },
28
+ { title: 'Created', dataIndex: 'created_at', sorter: true, width: '18%' },
29
+ { title: 'State', dataIndex: 'state_type', sorter: true, width: '14%' },
30
+ { title: 'Task Key', dataIndex: 'task_key', sorter: true, width: '22%' },
31
+ { title: 'Flow Run', dataIndex: 'flow_run_id', sorter: true, width: '22%' }
32
+ ],
33
+ onRecord: {
34
+ onClick: {
35
+ action: 'navigate',
36
+ url: '/task/{{record.id}}'
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ },
44
+ 'task/:id/*': {
45
+ layout: 'full_width',
46
+ components: {
47
+ main: [
48
+ {
49
+ component: 'RecordForm',
50
+ properties: {
51
+ table: 'task_run',
52
+ title: 'Task Run {{record.id}}',
53
+ readOnly: true
54
+ }
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,18 @@
1
+ import schema from './schema.js';
2
+ import upsert from './transforms/inbound/upsert_tables.js';
3
+ const metadata = {
4
+ name: '@engine9/schemas/timeline',
5
+ dependencies: {
6
+ '@engine9/schemas/person': '>=1.9.0'
7
+ }
8
+ };
9
+ export const transforms = {
10
+ upsert
11
+ };
12
+ export { metadata };
13
+ export { schema };
14
+ export default {
15
+ metadata,
16
+ schema,
17
+ transforms
18
+ };
@@ -0,0 +1,18 @@
1
+ export const tables = [
2
+ {
3
+ name: 'timeline',
4
+ columns: {
5
+ id: 'id_uuid',
6
+ ts: 'datetime',
7
+ input_id: 'uuid', // keep as uuid because it's required
8
+ entry_type_id: 'int',
9
+ person_id: 'person_id',
10
+ source_code_id: 'source_code_id',
11
+ created_at: 'created_at'
12
+ },
13
+ indexes: [{ columns: 'id', primary: true }, { columns: 'ts' }, { columns: 'person_id' }, { columns: 'input_id' }]
14
+ }
15
+ ];
16
+ export default {
17
+ tables
18
+ };
@@ -0,0 +1,22 @@
1
+ export const bindings = {
2
+ tablesToUpsert: { path: 'sql.tables.upsert' }
3
+ };
4
+ export async function transform({ batch, tablesToUpsert }) {
5
+ tablesToUpsert.timeline = tablesToUpsert.timeline || [];
6
+ batch.forEach((o) => {
7
+ if (o.timeline_id) {
8
+ tablesToUpsert.timeline.push({
9
+ id: o.timeline_id,
10
+ ts: o.ts || new Date(),
11
+ input_id: o.input_id,
12
+ entry_type_id: o.entry_type_id,
13
+ person_id: o.person_id,
14
+ source_code_id: o.source_code_id
15
+ });
16
+ }
17
+ });
18
+ }
19
+ export default {
20
+ bindings,
21
+ transform
22
+ };
@@ -0,0 +1,31 @@
1
+ {
2
+ routes: {
3
+ '/person/:id/*': {
4
+ components: {
5
+ sidebar: {
6
+ items: [
7
+ {
8
+ component: 'Link',
9
+ properties: {
10
+ label: 'Timeline',
11
+ to: 'timeline'
12
+ }
13
+ }
14
+ ]
15
+ },
16
+ main: [
17
+ {
18
+ path: 'timeline',
19
+ label: 'Timeline',
20
+ component: 'RecordDisplay',
21
+ properties: {
22
+ table: 'timeline',
23
+ conditions: [{ eql: 'person_id={{person_id}}' }],
24
+ rows: ['{{email}}']
25
+ }
26
+ }
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ import schema from './schema.js';
2
+
3
+ const metadata = {
4
+ name: '@engine9/schemas/transaction/contact_details',
5
+ dependencies: {
6
+ '@engine9/schemas/transaction/core': '>=1.9.0'
7
+ }
8
+ };
9
+
10
+ export { metadata, schema };
11
+ export default { metadata, schema };
@@ -0,0 +1,22 @@
1
+ // Contact details captured on a transaction: address, email, phone, employer, and occupation.
2
+ export const tables = [
3
+ {
4
+ name: 'transaction',
5
+ prefix: false,
6
+ columns: {
7
+ street_1: 'string',
8
+ street_2: 'string',
9
+ city: 'string',
10
+ region: 'string',
11
+ postal_code: { type: 'string', length: 16 },
12
+ country: 'string',
13
+ email: 'string',
14
+ phone: { type: 'string', length: 24 },
15
+ employer: 'string',
16
+ occupation: 'string'
17
+ },
18
+ indexes: []
19
+ }
20
+ ];
21
+
22
+ export default { tables };
@@ -0,0 +1,79 @@
1
+ # Transaction Core Schema Plugin
2
+
3
+ The `@engine9/schemas/transaction/core` plugin stores payment and gift transactions for people and provides the common transaction search, metrics, and segment used by imports, reports, and exports.
4
+
5
+ It depends on `@engine9/schemas/person` because every transaction belongs to a person through `person_id`.
6
+
7
+ ## Data Model
8
+
9
+ The plugin creates the `transaction` table. Each row is a dated amount tied to a person, an input, and optional source-code / message attribution.
10
+
11
+ | Field | Type | Description |
12
+ | --- | --- | --- |
13
+ | `id` | `id_uuid` | Primary row identifier. |
14
+ | `ts` | `datetime` | When the transaction occurred. |
15
+ | `input_id` | `uuid` | Input that produced the row. Required. |
16
+ | `entry_type_id` | `int` | Timeline entry type for the transaction. |
17
+ | `person_id` | `person_id` | Person the transaction is assigned to. |
18
+ | `amount` | `currency` | Transaction amount. |
19
+ | `remote_entry_id` | `string` | Remote system transaction id when available. |
20
+ | `remote_page_name` | `string` | Remote page or form name when available. |
21
+ | `remote_recurring_id` | `string` | Remote recurring-series id when available. |
22
+ | `recurs_id` | `int` | Recurring frequency. `0` is one-time. Defaults to `0`. |
23
+ | `recurring_number` | `int` | Sequence number in a recurring series. |
24
+ | `refund_amount` | `currency` | Refunded amount when applicable. |
25
+ | `given_name` / `family_name` / `email` | `string` | Snapshot of payer identity on the transaction. |
26
+ | `source_code_id` | `source_code_id` | Source code captured on the transaction. |
27
+ | `override_source_code_id` | `source_code_id` | Manual source-code override. |
28
+ | `final_source_code_id` | `source_code_id` | Source code after attribution. |
29
+ | `recommended_message_id` | `uuid` | Message suggested by last-click attribution. |
30
+ | `override_message_id` | `uuid` | Manual message override. |
31
+ | `final_message_id` | `uuid` | Message after attribution. |
32
+ | `extra` | `json` | Additional payload from the source system. |
33
+ | `created_at` | `created_at` | Timestamp when the row was created. |
34
+ | `modified_at` | `modified_at` | Timestamp when the row was last modified. |
35
+
36
+ Indexes:
37
+
38
+ - primary `id`
39
+ - `ts`
40
+ - `person_id`
41
+ - `remote_entry_id`
42
+ - `modified_at`
43
+
44
+ ## Inbound Behavior
45
+
46
+ The inbound `upsert` transform writes rows to `transaction`.
47
+
48
+ - Each row gets an `id` from the timeline-entry UUID helper and an `entry_type_id` from the entry type.
49
+ - When recurring fields are present, `recurs_id` is derived from `recurs` (`daily` through `annually`) or from `recurring_number` when no frequency is set.
50
+
51
+ ## Outbound Behavior
52
+
53
+ `appendTransactionSummary` looks up a person's transactions and attaches summary fields such as count, revenue, first/last/smallest/largest gifts, and recurring totals.
54
+
55
+ ## Search
56
+
57
+ - `all` finds people who have any transaction, optionally filtered by `plugin_id` and a `start` / `end` window on `ts` (ISO datetimes or relative expressions such as `-30d`).
58
+ - `minimum` finds people who have a transaction of at least a given `amount`.
59
+
60
+ ## Metrics
61
+
62
+ `summaryByDate` is a monthly card: record count and summed `amount` grouped by month of `ts`.
63
+
64
+ ## Segments
65
+
66
+ Predefined audiences shipped with this schema plugin. On install, each key becomes a `segment` row whose definition path is `@engine9/schemas/transaction/core:segments:<key>`.
67
+
68
+ ### Customers
69
+
70
+ | | |
71
+ | --- | --- |
72
+ | **Key** | `customers` |
73
+ | **Definition path** | `@engine9/schemas/transaction/core:segments:customers` |
74
+ | **Who is included** | People who have at least one row in `transaction` |
75
+ | **Who is excluded** | People with no transactions |
76
+ | **How it is built** | A `transaction` table search that selects `person_id` with no extra filters |
77
+ | **Universe** | None — any transaction from any plugin, date, or amount counts |
78
+
79
+ Refunds, recurring gifts, and $0 rows are not filtered out. If a person has ever appeared on a transaction row, they are a customer.