@fonderie/customers 1.1.0 → 1.1.2

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.
@@ -0,0 +1,189 @@
1
+ <!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
2
+
3
+ # @fonderie/customers — outcomes
4
+
5
+ What this package does to a running app: tables its migrations create,
6
+ rows it seeds, routes it registers. Generated from the migration SQL and
7
+ route tables in source — trust this file instead of reading `dist/` or
8
+ downloading tarballs.
9
+
10
+ ## Database tables (after all migrations)
11
+
12
+ ### `fonderie_addresses`
13
+
14
+ ```sql
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
16
+ country_iso TEXT NOT NULL
17
+ subdivision1_iso TEXT
18
+ subdivision2_iso TEXT
19
+ zip_postal_code TEXT NOT NULL
20
+ line1 TEXT
21
+ line2 TEXT
22
+ unit TEXT
23
+ ```
24
+
25
+ ### `fonderie_customer_addresses`
26
+
27
+ ```sql
28
+ addr_id UUID NOT NULL REFERENCES fonderie_addresses(id) ON DELETE CASCADE
29
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
30
+ label TEXT NOT NULL DEFAULT 'service'
31
+ is_primary BOOLEAN NOT NULL DEFAULT false
32
+ label_id UUID REFERENCES fonderie_customer_labels(id)
33
+ CONSTRAINT fonderie_customer_addresses_label_id_fkey FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL
34
+ -- PRIMARY KEY (addr_id, customer_id)
35
+ ```
36
+
37
+ ### `fonderie_customer_emails`
38
+
39
+ ```sql
40
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
41
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
42
+ email TEXT NOT NULL
43
+ label TEXT NOT NULL DEFAULT 'work'
44
+ is_primary BOOLEAN NOT NULL DEFAULT false
45
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
46
+ label_id UUID REFERENCES fonderie_customer_labels(id)
47
+ CONSTRAINT fonderie_customer_emails_label_id_fkey FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL
48
+ ```
49
+
50
+ ### `fonderie_customer_labels`
51
+
52
+ ```sql
53
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
54
+ type fonderie_customer_label_type NOT NULL
55
+ value TEXT NOT NULL
56
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
57
+ -- CONSTRAINT uq_fcl_type_value UNIQUE (type, value)
58
+ ```
59
+
60
+ ### `fonderie_customer_notes`
61
+
62
+ ```sql
63
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
64
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
65
+ author_id UUID
66
+ body TEXT NOT NULL
67
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
68
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
69
+ ```
70
+
71
+ ### `fonderie_customer_phones`
72
+
73
+ ```sql
74
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
75
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
76
+ phone TEXT NOT NULL
77
+ label TEXT NOT NULL DEFAULT 'mobile'
78
+ is_primary BOOLEAN NOT NULL DEFAULT false
79
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
80
+ label_id UUID REFERENCES fonderie_customer_labels(id)
81
+ CONSTRAINT fonderie_customer_phones_label_id_fkey FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL
82
+ ```
83
+
84
+ ### `fonderie_customer_relationships`
85
+
86
+ ```sql
87
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
88
+ workspace_id UUID NOT NULL
89
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
90
+ related_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
91
+ relationship TEXT NOT NULL
92
+ is_primary BOOLEAN NOT NULL DEFAULT false
93
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
94
+ -- UNIQUE (customer_id, related_id)
95
+ ```
96
+
97
+ ### `fonderie_customer_sequences`
98
+
99
+ ```sql
100
+ workspace_id UUID NOT NULL
101
+ prefix TEXT NOT NULL
102
+ next_val BIGINT NOT NULL DEFAULT 1
103
+ -- PRIMARY KEY (workspace_id, prefix)
104
+ ```
105
+
106
+ ### `fonderie_customer_tags`
107
+
108
+ ```sql
109
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE
110
+ tag TEXT NOT NULL
111
+ -- PRIMARY KEY (customer_id, tag)
112
+ ```
113
+
114
+ ### `fonderie_customers`
115
+
116
+ ```sql
117
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
118
+ workspace_id UUID NOT NULL
119
+ type TEXT NOT NULL DEFAULT 'individual'
120
+ first_name TEXT
121
+ last_name TEXT
122
+ company_name TEXT
123
+ avatar_url TEXT
124
+ locale TEXT NOT NULL DEFAULT 'en-US'
125
+ reference_code TEXT
126
+ is_blacklisted BOOLEAN NOT NULL DEFAULT false
127
+ created_by UUID
128
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
129
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
130
+ sex TEXT NOT NULL DEFAULT 'UNKNOWN'
131
+ blacklist_reason TEXT
132
+ ```
133
+
134
+ Raw SQL ships in `node_modules/@fonderie/customers/dist/migrations/sql/` — read it there if you must; never download tarballs.
135
+
136
+ ## Seeded rows (behavioral contract)
137
+
138
+ ```sql
139
+ INSERT INTO fonderie_customer_labels (type, value) VALUES ('email', 'work'), ('email', 'personal'), ('email', 'billing'), ('phone', 'mobile'), ('phone', 'office'), ('phone', 'home'), ('phone', 'fax'), ('address', 'service'), ('address', 'billing'), ('address', 'other') ON CONFLICT (type, value) DO NOTHING;
140
+ INSERT INTO fonderie_customer_labels (type, value) SELECT DISTINCT 'email'::fonderie_customer_label_type, label FROM fonderie_customer_emails WHERE label IS NOT NULL ON CONFLICT (type, value) DO NOTHING;
141
+ INSERT INTO fonderie_customer_labels (type, value) SELECT DISTINCT 'phone'::fonderie_customer_label_type, label FROM fonderie_customer_phones WHERE label IS NOT NULL ON CONFLICT (type, value) DO NOTHING;
142
+ INSERT INTO fonderie_customer_labels (type, value) SELECT DISTINCT 'address'::fonderie_customer_label_type, label FROM fonderie_customer_addresses WHERE label IS NOT NULL ON CONFLICT (type, value) DO NOTHING;
143
+ ```
144
+
145
+ ## HTTP routes registered
146
+
147
+ | Method | Path | Middleware chain (auth / validation / handler) |
148
+ |---|---|---|
149
+ | GET | `/customers` | `requireAuth → wsCtx → customer.list` |
150
+ | POST | `/customers` | `requireAuth → wsCtx → validate(createCustomerSchema) → customer.create` |
151
+ | DELETE | `/customers/:customerId` | `requireAuth → wsCtx → customer.delete` |
152
+ | GET | `/customers/:customerId` | `requireAuth → wsCtx → customer.get` |
153
+ | PUT | `/customers/:customerId` | `requireAuth → wsCtx → validate(updateCustomerSchema) → customer.update` |
154
+ | GET | `/customers/:customerId/addresses` | `requireAuth → wsCtx → address.list` |
155
+ | POST | `/customers/:customerId/addresses` | `requireAuth → wsCtx → validate(addAddressSchema) → address.add` |
156
+ | DELETE | `/customers/:customerId/addresses/:addrId` | `requireAuth → wsCtx → address.remove` |
157
+ | PATCH | `/customers/:customerId/addresses/:addrId` | `requireAuth → wsCtx → validate(updateAddressSchema) → address.update` |
158
+ | PUT | `/customers/:customerId/addresses/:addrId/primary` | `requireAuth → wsCtx → address.setPrimary` |
159
+ | POST | `/customers/:customerId/blacklist` | `requireAuth → wsCtx → validate(blacklistSchema) → customer.blacklist` |
160
+ | GET | `/customers/:customerId/emails` | `requireAuth → wsCtx → email.list` |
161
+ | POST | `/customers/:customerId/emails` | `requireAuth → wsCtx → validate(addEmailSchema) → email.add` |
162
+ | DELETE | `/customers/:customerId/emails/:emailId` | `requireAuth → wsCtx → email.remove` |
163
+ | PATCH | `/customers/:customerId/emails/:emailId` | `requireAuth → wsCtx → validate(updateEmailSchema) → email.update` |
164
+ | PUT | `/customers/:customerId/emails/:emailId/primary` | `requireAuth → wsCtx → email.setPrimary` |
165
+ | GET | `/customers/:customerId/notes` | `requireAuth → wsCtx → note.list` |
166
+ | POST | `/customers/:customerId/notes` | `requireAuth → wsCtx → validate(noteSchema) → note.create` |
167
+ | DELETE | `/customers/:customerId/notes/:noteId` | `requireAuth → wsCtx → note.delete` |
168
+ | PUT | `/customers/:customerId/notes/:noteId` | `requireAuth → wsCtx → validate(noteSchema) → note.update` |
169
+ | GET | `/customers/:customerId/phones` | `requireAuth → wsCtx → phone.list` |
170
+ | POST | `/customers/:customerId/phones` | `requireAuth → wsCtx → validate(addPhoneSchema) → phone.add` |
171
+ | DELETE | `/customers/:customerId/phones/:phoneId` | `requireAuth → wsCtx → phone.remove` |
172
+ | PATCH | `/customers/:customerId/phones/:phoneId` | `requireAuth → wsCtx → validate(updatePhoneSchema) → phone.update` |
173
+ | PUT | `/customers/:customerId/phones/:phoneId/primary` | `requireAuth → wsCtx → phone.setPrimary` |
174
+ | GET | `/customers/:customerId/relationships` | `requireAuth → wsCtx → relationship.list` |
175
+ | POST | `/customers/:customerId/relationships` | `requireAuth → wsCtx → validate(addRelationshipSchema) → relationship.add` |
176
+ | DELETE | `/customers/:customerId/relationships/:relatedId` | `requireAuth → wsCtx → relationship.remove` |
177
+ | PUT | `/customers/:customerId/relationships/:relatedId/primary` | `requireAuth → wsCtx → relationship.setPrimary` |
178
+ | GET | `/customers/:customerId/tags` | `requireAuth → wsCtx → tag.list` |
179
+ | POST | `/customers/:customerId/tags` | `requireAuth → wsCtx → validate(addTagSchema) → tag.add` |
180
+ | DELETE | `/customers/:customerId/tags/:tag` | `requireAuth → wsCtx → tag.remove` |
181
+ | POST | `/customers/:customerId/unblacklist` | `requireAuth → wsCtx → customer.unblacklist` |
182
+ | GET | `/customers/labels` | `requireAuth → wsCtx → label.list` |
183
+ | DELETE | `/customers/labels/:labelId` | `requireAuth → wsCtx → label.remove` |
184
+
185
+ ## Migration statements not replayed (verify in raw SQL)
186
+
187
+ - `END IF`
188
+ - `END $$`
189
+ - `CREATE TYPE fonderie_customer_label_type AS ENUM ('email', 'phone', 'address')`
@@ -0,0 +1,244 @@
1
+ <!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
2
+
3
+ # @fonderie/customers — signatures
4
+
5
+ ## @fonderie/customers
6
+
7
+ Subpath exports: `@fonderie/customers/types`, `@fonderie/customers/migrations`
8
+
9
+ ```ts
10
+ type CustomersEventKey = (typeof EVENT_KEYS)[keyof typeof EVENT_KEYS];
11
+
12
+ type ICustomersConfig = {
13
+ referenceCodePrefix?: string;
14
+ };
15
+
16
+ const EVENT_KEYS: { readonly customerCreated: "fonderie.customer.created"; readonly customerUpdated: "fonderie.customer.updated"; readonly customerDeleted: "fonderie.customer.deleted"; readonly customerBlacklisted: "fonderie.customer.blacklisted"; readonly customerUnblacklisted: "fonderie.customer.unblacklisted"; }
17
+
18
+ interface IAddressDTO {
19
+ countryIso: string;
20
+ subdivision1Iso: string;
21
+ subdivision2Iso: string;
22
+ zipPostalCode: string;
23
+ unit: string;
24
+ line1: string;
25
+ line2: string;
26
+ }
27
+
28
+ interface ICustomerAddressDTO {
29
+ id: string;
30
+ label: string;
31
+ isPrimary: boolean;
32
+ address: IAddressDTO;
33
+ }
34
+
35
+ interface ICustomerDetailDTO extends ICustomerDTO {
36
+ emails: ICustomerEmailDTO[];
37
+ phones: ICustomerPhoneDTO[];
38
+ addresses: ICustomerAddressDTO[];
39
+ notes: ICustomerNoteDTO[];
40
+ relationships: ICustomerRelationshipExpandedDTO[];
41
+ tags: string[];
42
+ }
43
+
44
+ interface ICustomerDTO {
45
+ id: string;
46
+ type: string;
47
+ sex: CustomerSex;
48
+ firstName: string;
49
+ lastName: string;
50
+ companyName: string;
51
+ avatarUrl: string;
52
+ locale: string;
53
+ referenceCode: string;
54
+ blacklisted: {
55
+ status: boolean;
56
+ reason: string | null;
57
+ };
58
+ createdBy: string;
59
+ createdAt: string;
60
+ updatedAt: string;
61
+ }
62
+
63
+ interface ICustomerEmailDTO {
64
+ id: string;
65
+ email: string;
66
+ label: string;
67
+ isPrimary: boolean;
68
+ createdAt: string;
69
+ }
70
+
71
+ interface ICustomerNoteDTO {
72
+ id: string;
73
+ authorId: string;
74
+ body: string;
75
+ createdAt: string;
76
+ updatedAt: string;
77
+ }
78
+
79
+ interface ICustomerPhoneDTO {
80
+ id: string;
81
+ phone: string;
82
+ label: string;
83
+ isPrimary: boolean;
84
+ createdAt: string;
85
+ }
86
+
87
+ interface ICustomerTagDTO {
88
+ tag: string;
89
+ }
90
+
91
+ function toAddressDTO(a: IAddress): IAddressDTO
92
+
93
+ function toCustomerAddressDTO(ca: ICustomerAddress): ICustomerAddressDTO
94
+
95
+ function toCustomerDetailDTO(c: ICustomerDetail): ICustomerDetailDTO
96
+
97
+ function toCustomerDTO(c: ICustomer): ICustomerDTO
98
+
99
+ function toCustomerEmailDTO(e: ICustomerEmail): ICustomerEmailDTO
100
+
101
+ function toCustomerNoteDTO(n: ICustomerNote): ICustomerNoteDTO
102
+
103
+ function toCustomerPhoneDTO(p: ICustomerPhone): ICustomerPhoneDTO
104
+
105
+ function toCustomerTagDTO(t: ICustomerTag): ICustomerTagDTO
106
+
107
+ new CustomerAddressModel(store: IStoreAdapter): CustomerAddressModel
108
+ .list(customerId: string): Promise<ICustomerAddress[]>
109
+ .add(opts: { customerId: string; countryIso: string; subdivision1Iso?: string | null; subdivision2Iso?: string | null; zipPostalCode: string; unit?: string | null; line1?: string | null; line2?: string | null; labelId: string; isPrimary?: boolean; }): Promise<...>
110
+ .updateLabel(addrId: string, customerId: string, labelId: string): Promise<ICustomerAddress>
111
+ .setPrimary(addrId: string, customerId: string): Promise<void>
112
+ .remove(addrId: string, customerId: string): Promise<void>
113
+
114
+ new CustomerEmailModel(store: IStoreAdapter): CustomerEmailModel
115
+ .list(customerId: string): Promise<ICustomerEmail[]>
116
+ .add(opts: { customerId: string; email: string; labelId: string; isPrimary?: boolean; }): Promise<ICustomerEmail>
117
+ .updateLabel(emailId: string, customerId: string, labelId: string): Promise<ICustomerEmail>
118
+ .setPrimary(emailId: string, customerId: string): Promise<void>
119
+ .remove(emailId: string, customerId: string): Promise<void>
120
+
121
+ new CustomerModel(store: IStoreAdapter): CustomerModel
122
+ .list(opts: ListCustomersOpts): Promise<ICustomer[]>
123
+ .findById(id: string, workspaceId: string): Promise<ICustomer | null>
124
+ .findDetail(id: string, workspaceId: string, depth: 2): Promise<ICustomerDetailD2 | null>
125
+ .create(opts: CreateCustomerOpts): Promise<ICustomer>
126
+ .update(id: string, workspaceId: string, opts: UpdateCustomerOpts, referenceCodePrefix?: string): Promise<ICustomer | null>
127
+ .delete(id: string, workspaceId: string): Promise<void>
128
+ .blacklist(id: string, workspaceId: string, reason?: string | null | undefined): Promise<void>
129
+ .unblacklist(id: string, workspaceId: string): Promise<void>
130
+
131
+ new CustomerNoteModel(store: IStoreAdapter): CustomerNoteModel
132
+ .list(customerId: string): Promise<ICustomerNote[]>
133
+ .create(opts: { customerId: string; authorId?: string | null; body: string; }): Promise<ICustomerNote>
134
+ .update(noteId: string, customerId: string, body: string): Promise<ICustomerNote | null>
135
+ .delete(noteId: string, customerId: string): Promise<void>
136
+
137
+ new CustomerPhoneModel(store: IStoreAdapter): CustomerPhoneModel
138
+ .list(customerId: string): Promise<ICustomerPhone[]>
139
+ .add(opts: { customerId: string; phone: string; labelId: string; isPrimary?: boolean; }): Promise<ICustomerPhone>
140
+ .updateLabel(phoneId: string, customerId: string, labelId: string): Promise<ICustomerPhone>
141
+ .setPrimary(phoneId: string, customerId: string): Promise<void>
142
+ .remove(phoneId: string, customerId: string): Promise<void>
143
+
144
+ new CustomerTagModel(store: IStoreAdapter): CustomerTagModel
145
+ .list(customerId: string): Promise<string[]>
146
+ .add(customerId: string, tag: string): Promise<void>
147
+ .remove(customerId: string, tag: string): Promise<void>
148
+
149
+ new CustomersModule(store: IStoreAdapter, config?: ICustomersConfig, bus?: EventBus | undefined): CustomersModule
150
+ .name: "@fonderie/customers"
151
+ .deps: string[]
152
+ .install(app: IFonderieApp): void
153
+
154
+ type AddressLabel = 'service' | 'billing' | 'other';
155
+
156
+ type CustomerType = 'individual' | 'business';
157
+
158
+ type EmailLabel = 'work' | 'personal' | 'billing';
159
+
160
+ interface IAddress {
161
+ id: string;
162
+ countryIso: string;
163
+ subdivision1Iso: string | null;
164
+ subdivision2Iso: string | null;
165
+ zipPostalCode: string;
166
+ unit: string | null;
167
+ line1: string | null;
168
+ line2: string | null;
169
+ }
170
+
171
+ interface ICustomer {
172
+ id: string;
173
+ workspaceId: string;
174
+ type: CustomerType;
175
+ sex: CustomerSex;
176
+ firstName: string | null;
177
+ lastName: string | null;
178
+ companyName: string | null;
179
+ avatarUrl: string | null;
180
+ locale: string;
181
+ referenceCode: string | null;
182
+ isBlacklisted: boolean;
183
+ blacklistReason: string | null;
184
+ createdBy: string | null;
185
+ createdAt: string;
186
+ updatedAt: string;
187
+ }
188
+
189
+ interface ICustomerAddress {
190
+ addrId: string;
191
+ customerId: string;
192
+ labelId: string;
193
+ label: string;
194
+ isPrimary: boolean;
195
+ address: IAddress;
196
+ }
197
+
198
+ interface ICustomerDetail extends ICustomer {
199
+ emails: ICustomerEmail[];
200
+ phones: ICustomerPhone[];
201
+ addresses: ICustomerAddress[];
202
+ notes: ICustomerNote[];
203
+ relationships: ICustomerRelationshipExpanded[];
204
+ tags: string[];
205
+ }
206
+
207
+ interface ICustomerEmail {
208
+ id: string;
209
+ customerId: string;
210
+ email: string;
211
+ labelId: string;
212
+ label: string;
213
+ isPrimary: boolean;
214
+ createdAt: string;
215
+ }
216
+
217
+ interface ICustomerNote {
218
+ id: string;
219
+ customerId: string;
220
+ authorId: string | null;
221
+ body: string;
222
+ createdAt: string;
223
+ updatedAt: string;
224
+ }
225
+
226
+ interface ICustomerPhone {
227
+ id: string;
228
+ customerId: string;
229
+ phone: string;
230
+ labelId: string;
231
+ label: string;
232
+ isPrimary: boolean;
233
+ createdAt: string;
234
+ }
235
+
236
+ interface ICustomerTag {
237
+ customerId: string;
238
+ tag: string;
239
+ }
240
+
241
+ type PhoneLabel = 'mobile' | 'office' | 'home' | 'fax';
242
+
243
+ namespace schemas — exports: addAddressSchema, addEmailSchema, addPhoneSchema, addRelationshipSchema, addTagSchema, blacklistSchema, createCustomerSchema, noteSchema, updateAddressSchema, updateCustomerSchema, updateEmailSchema, updatePhoneSchema
244
+ ```
@@ -0,0 +1,111 @@
1
+ -- fonderie_customers
2
+ CREATE TABLE IF NOT EXISTS fonderie_customers (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ workspace_id UUID NOT NULL,
5
+ type TEXT NOT NULL DEFAULT 'individual',
6
+ first_name TEXT,
7
+ last_name TEXT,
8
+ company_name TEXT,
9
+ job_title TEXT,
10
+ avatar_url TEXT,
11
+ locale TEXT NOT NULL DEFAULT 'en-US',
12
+ reference_code TEXT,
13
+ is_blacklisted BOOLEAN NOT NULL DEFAULT false,
14
+ created_by UUID,
15
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
16
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_fc_workspace
20
+ ON fonderie_customers (workspace_id);
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_fc_workspace_blacklisted
23
+ ON fonderie_customers (workspace_id, is_blacklisted);
24
+
25
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fc_reference_code
26
+ ON fonderie_customers (workspace_id, reference_code)
27
+ WHERE reference_code IS NOT NULL;
28
+
29
+ -- fonderie_addresses (shared; created IF NOT EXISTS so another package can also define it)
30
+ CREATE TABLE IF NOT EXISTS fonderie_addresses (
31
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32
+ country_iso TEXT NOT NULL,
33
+ subdivision1_iso TEXT,
34
+ subdivision2_iso TEXT,
35
+ zip_postal_code TEXT NOT NULL,
36
+ line1 TEXT,
37
+ line2 TEXT
38
+ );
39
+
40
+ -- fonderie_customer_emails
41
+ CREATE TABLE IF NOT EXISTS fonderie_customer_emails (
42
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
43
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
44
+ email TEXT NOT NULL,
45
+ label TEXT NOT NULL DEFAULT 'work',
46
+ is_primary BOOLEAN NOT NULL DEFAULT false,
47
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_fce_customer
51
+ ON fonderie_customer_emails (customer_id);
52
+
53
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fce_primary
54
+ ON fonderie_customer_emails (customer_id)
55
+ WHERE is_primary = true;
56
+
57
+ -- fonderie_customer_phones
58
+ CREATE TABLE IF NOT EXISTS fonderie_customer_phones (
59
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
60
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
61
+ phone TEXT NOT NULL,
62
+ label TEXT NOT NULL DEFAULT 'mobile',
63
+ is_primary BOOLEAN NOT NULL DEFAULT false,
64
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
65
+ );
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_fcp_customer
68
+ ON fonderie_customer_phones (customer_id);
69
+
70
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fcp_primary
71
+ ON fonderie_customer_phones (customer_id)
72
+ WHERE is_primary = true;
73
+
74
+ -- fonderie_customer_addresses (junction)
75
+ CREATE TABLE IF NOT EXISTS fonderie_customer_addresses (
76
+ addr_id UUID NOT NULL REFERENCES fonderie_addresses(id) ON DELETE CASCADE,
77
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
78
+ label TEXT NOT NULL DEFAULT 'service',
79
+ is_primary BOOLEAN NOT NULL DEFAULT false,
80
+ PRIMARY KEY (addr_id, customer_id)
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_fca_customer
84
+ ON fonderie_customer_addresses (customer_id);
85
+
86
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fca_primary
87
+ ON fonderie_customer_addresses (customer_id)
88
+ WHERE is_primary = true;
89
+
90
+ -- fonderie_customer_notes
91
+ CREATE TABLE IF NOT EXISTS fonderie_customer_notes (
92
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
93
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
94
+ author_id UUID,
95
+ body TEXT NOT NULL,
96
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
97
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_fcn_customer
101
+ ON fonderie_customer_notes (customer_id);
102
+
103
+ -- fonderie_customer_tags
104
+ CREATE TABLE IF NOT EXISTS fonderie_customer_tags (
105
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
106
+ tag TEXT NOT NULL,
107
+ PRIMARY KEY (customer_id, tag)
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_fct_tag
111
+ ON fonderie_customer_tags (tag);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE fonderie_customers
2
+ ADD COLUMN IF NOT EXISTS sex TEXT NOT NULL DEFAULT 'UNKNOWN';
@@ -0,0 +1,6 @@
1
+ CREATE TABLE IF NOT EXISTS fonderie_customer_sequences (
2
+ workspace_id UUID NOT NULL,
3
+ prefix TEXT NOT NULL,
4
+ next_val BIGINT NOT NULL DEFAULT 1,
5
+ PRIMARY KEY (workspace_id, prefix)
6
+ );
@@ -0,0 +1 @@
1
+ -- No-op: sequence table stays (workspace_id, prefix) — per-prefix counters are intentional.
@@ -0,0 +1 @@
1
+ ALTER TABLE fonderie_customers DROP COLUMN IF EXISTS job_title;
@@ -0,0 +1,23 @@
1
+ -- Remove duplicate emails per customer, keeping the primary or the oldest row.
2
+ DELETE FROM fonderie_customer_emails
3
+ WHERE id NOT IN (
4
+ SELECT DISTINCT ON (customer_id, email)
5
+ id
6
+ FROM fonderie_customer_emails
7
+ ORDER BY customer_id, email, is_primary DESC, created_at ASC
8
+ );
9
+
10
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fce_unique_email
11
+ ON fonderie_customer_emails (customer_id, email);
12
+
13
+ -- Remove duplicate phones per customer, keeping the primary or the oldest row.
14
+ DELETE FROM fonderie_customer_phones
15
+ WHERE id NOT IN (
16
+ SELECT DISTINCT ON (customer_id, phone)
17
+ id
18
+ FROM fonderie_customer_phones
19
+ ORDER BY customer_id, phone, is_primary DESC, created_at ASC
20
+ );
21
+
22
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fcp_unique_phone
23
+ ON fonderie_customer_phones (customer_id, phone);
@@ -0,0 +1,13 @@
1
+ DO $$
2
+ BEGIN
3
+ IF EXISTS (
4
+ SELECT 1 FROM information_schema.columns
5
+ WHERE table_name = 'fonderie_customers' AND column_name = 'is_archived'
6
+ ) THEN
7
+ ALTER TABLE fonderie_customers RENAME COLUMN is_archived TO is_blacklisted;
8
+ END IF;
9
+ END $$;
10
+
11
+ DROP INDEX IF EXISTS idx_fc_workspace_archived;
12
+ CREATE INDEX IF NOT EXISTS idx_fc_workspace_blacklisted
13
+ ON fonderie_customers (workspace_id, is_blacklisted);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE fonderie_customers
2
+ ADD COLUMN IF NOT EXISTS blacklist_reason TEXT;
@@ -0,0 +1,25 @@
1
+ -- Unit number on shared addresses table
2
+ ALTER TABLE fonderie_addresses
3
+ ADD COLUMN IF NOT EXISTS unit TEXT;
4
+
5
+ -- Links two customers with a typed, directional relationship
6
+ CREATE TABLE IF NOT EXISTS fonderie_customer_relationships (
7
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ workspace_id UUID NOT NULL,
9
+ customer_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
10
+ related_id UUID NOT NULL REFERENCES fonderie_customers(id) ON DELETE CASCADE,
11
+ relationship TEXT NOT NULL,
12
+ is_primary BOOLEAN NOT NULL DEFAULT false,
13
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
14
+ UNIQUE (customer_id, related_id)
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_fcrel_customer
18
+ ON fonderie_customer_relationships (customer_id);
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_fcrel_related
21
+ ON fonderie_customer_relationships (related_id);
22
+
23
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fcrel_primary
24
+ ON fonderie_customer_relationships (customer_id)
25
+ WHERE is_primary = true;
@@ -0,0 +1,75 @@
1
+ -- fonderie_customer_label_type enum
2
+ CREATE TYPE fonderie_customer_label_type AS ENUM ('email', 'phone', 'address');
3
+
4
+ -- shared label catalog
5
+ CREATE TABLE IF NOT EXISTS fonderie_customer_labels (
6
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ type fonderie_customer_label_type NOT NULL,
8
+ value TEXT NOT NULL,
9
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10
+ CONSTRAINT uq_fcl_type_value UNIQUE (type, value)
11
+ );
12
+
13
+ -- seed defaults
14
+ INSERT INTO fonderie_customer_labels (type, value) VALUES
15
+ ('email', 'work'),
16
+ ('email', 'personal'),
17
+ ('email', 'billing'),
18
+ ('phone', 'mobile'),
19
+ ('phone', 'office'),
20
+ ('phone', 'home'),
21
+ ('phone', 'fax'),
22
+ ('address', 'service'),
23
+ ('address', 'billing'),
24
+ ('address', 'other')
25
+ ON CONFLICT (type, value) DO NOTHING;
26
+
27
+ -- promote any non-standard values already stored in existing rows
28
+ INSERT INTO fonderie_customer_labels (type, value)
29
+ SELECT DISTINCT 'email'::fonderie_customer_label_type, label
30
+ FROM fonderie_customer_emails
31
+ WHERE label IS NOT NULL
32
+ ON CONFLICT (type, value) DO NOTHING;
33
+
34
+ INSERT INTO fonderie_customer_labels (type, value)
35
+ SELECT DISTINCT 'phone'::fonderie_customer_label_type, label
36
+ FROM fonderie_customer_phones
37
+ WHERE label IS NOT NULL
38
+ ON CONFLICT (type, value) DO NOTHING;
39
+
40
+ INSERT INTO fonderie_customer_labels (type, value)
41
+ SELECT DISTINCT 'address'::fonderie_customer_label_type, label
42
+ FROM fonderie_customer_addresses
43
+ WHERE label IS NOT NULL
44
+ ON CONFLICT (type, value) DO NOTHING;
45
+
46
+ -- add label_id FK columns
47
+ ALTER TABLE fonderie_customer_emails
48
+ ADD COLUMN IF NOT EXISTS label_id UUID REFERENCES fonderie_customer_labels(id);
49
+
50
+ ALTER TABLE fonderie_customer_phones
51
+ ADD COLUMN IF NOT EXISTS label_id UUID REFERENCES fonderie_customer_labels(id);
52
+
53
+ ALTER TABLE fonderie_customer_addresses
54
+ ADD COLUMN IF NOT EXISTS label_id UUID REFERENCES fonderie_customer_labels(id);
55
+
56
+ -- backfill label_id from existing label text
57
+ UPDATE fonderie_customer_emails e
58
+ SET label_id = l.id
59
+ FROM fonderie_customer_labels l
60
+ WHERE l.type = 'email' AND l.value = e.label;
61
+
62
+ UPDATE fonderie_customer_phones p
63
+ SET label_id = l.id
64
+ FROM fonderie_customer_labels l
65
+ WHERE l.type = 'phone' AND l.value = p.label;
66
+
67
+ UPDATE fonderie_customer_addresses a
68
+ SET label_id = l.id
69
+ FROM fonderie_customer_labels l
70
+ WHERE l.type = 'address' AND l.value = a.label;
71
+
72
+ -- mark old text columns as deprecated
73
+ COMMENT ON COLUMN fonderie_customer_emails.label IS 'deprecated: use label_id → fonderie_customer_labels';
74
+ COMMENT ON COLUMN fonderie_customer_phones.label IS 'deprecated: use label_id → fonderie_customer_labels';
75
+ COMMENT ON COLUMN fonderie_customer_addresses.label IS 'deprecated: use label_id → fonderie_customer_labels';
@@ -0,0 +1,17 @@
1
+ -- Allow label catalog entries to be deleted even when in use.
2
+ -- Affected phone/email/address rows keep their label text column; label_id becomes NULL.
3
+
4
+ ALTER TABLE fonderie_customer_phones
5
+ DROP CONSTRAINT IF EXISTS fonderie_customer_phones_label_id_fkey,
6
+ ADD CONSTRAINT fonderie_customer_phones_label_id_fkey
7
+ FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL;
8
+
9
+ ALTER TABLE fonderie_customer_emails
10
+ DROP CONSTRAINT IF EXISTS fonderie_customer_emails_label_id_fkey,
11
+ ADD CONSTRAINT fonderie_customer_emails_label_id_fkey
12
+ FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL;
13
+
14
+ ALTER TABLE fonderie_customer_addresses
15
+ DROP CONSTRAINT IF EXISTS fonderie_customer_addresses_label_id_fkey,
16
+ ADD CONSTRAINT fonderie_customer_addresses_label_id_fkey
17
+ FOREIGN KEY (label_id) REFERENCES fonderie_customer_labels(id) ON DELETE SET NULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/customers",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Workspace-scoped customer records — individuals and businesses with multiple emails, phones, addresses, notes, and tags.",
5
5
  "keywords": [
6
6
  "fonderie-js",
@@ -70,17 +70,18 @@
70
70
  },
71
71
  "files": [
72
72
  "dist",
73
+ "brain",
73
74
  "LICENSE",
74
75
  "README.md"
75
76
  ],
76
77
  "repository": {
77
78
  "type": "git",
78
- "url": "git+https://github.com/fonderie-js/sdk.git",
79
+ "url": "git+https://github.com/fonderiejs/sdk.git",
79
80
  "directory": "packages/customers"
80
81
  },
81
- "homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/customers#readme",
82
+ "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/customers#readme",
82
83
  "bugs": {
83
- "url": "https://github.com/fonderie-js/sdk/issues"
84
+ "url": "https://github.com/fonderiejs/sdk/issues"
84
85
  },
85
86
  "dependencies": {
86
87
  "zod": "^4.4.3"