rhachet-roles-ehmpathy 1.7.0 → 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.
- package/dist/contract/commands/codegenBriefOptions.js +1 -1
- package/dist/contract/commands/codegenBriefOptions.js.map +1 -1
- package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].v3.md +87 -0
- package/dist/logic/roles/architect/.briefs/practices/prefer.env_access.prep_over_dev.md +12 -0
- package/dist/logic/roles/architect/getArchitectRole.d.ts +2 -0
- package/dist/logic/roles/architect/getArchitectRole.js +27 -0
- package/dist/logic/roles/architect/getArchitectRole.js.map +1 -0
- package/dist/logic/roles/bhrain/getBhrainRole.js +19 -14
- package/dist/logic/roles/bhrain/getBhrainRole.js.map +1 -1
- package/dist/logic/roles/ecologist/getEcologistRole.js +12 -7
- package/dist/logic/roles/ecologist/getEcologistRole.js.map +1 -1
- package/dist/logic/roles/getRoleRegistry.js +2 -0
- package/dist/logic/roles/getRoleRegistry.js.map +1 -1
- package/dist/logic/roles/mechanic/.briefs/architecture/directional-dependencies.md +49 -40
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/never.term.script.md +7 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/prefer.emojis.chill_nature.md +24 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/prefer.jq.over_alt.[demo].md +29 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/prefer.terraform.[criteria].md +4 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/prefer.terraform.[seed].md +17 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.dependency.pinned_versions.md +3 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.idempotency.md +33 -0
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.knowledge.externalized.md +17 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/bad-practice/forbid.positional-args.md +43 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/best-practice/require.namedargs.md +6 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/.readme.md +0 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/best-practice/declastruct.[demo].md +485 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.nullable.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.undefined.md +15 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.refs.immuatble.md +9 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/best-practice/ref.package.domain-objects.[readme].md +585 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.operations/best-practice/require.sync.names.md +14 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/bad-practices/forbid.hide_errors.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.[demo].shell.md +17 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.md +28 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/directional-dependencies.md +82 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/dot-test-and-dot-temp.md +20 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.typescript.utils/best-practice/ref.package.as-command.[tips].md +7 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.acceptance/best-practice/blackbox.md +5 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.run.[lesson].md +18 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.use.[lesson].md +20 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].md +3 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_integ.md +8 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_units.md +9 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md +280 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/prefer.datadriven.md +41 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/ref.test-fns.[readme].md +185 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/whento.snapshots.[lesson].md +23 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/.readme.md +1 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=practices.terms=forbid_prefer_desire_require.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=software.terms=prodcode_vs_testcode.md +7 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/.readme.md +3 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.chill-nature.md +0 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.lowercase.md +0 -0
- package/dist/logic/roles/mechanic/getMechanicRole.js +7 -2
- package/dist/logic/roles/mechanic/getMechanicRole.js.map +1 -1
- package/dist/logic/roles/terminal.commander/getCommanderRole.js +7 -2
- package/dist/logic/roles/terminal.commander/getCommanderRole.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# .brief.demo = declastruct pattern, implemented for the stripe sdk
|
|
2
|
+
|
|
3
|
+
Use the declastruct pattern whenever we need to construct and control remote resources. Each remote resource is considered a structure that we declaratively control, via idempotent get+set semantics.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### 1. first, declare the entities we wish to construct
|
|
7
|
+
|
|
8
|
+
declare them as explicit domain-objects
|
|
9
|
+
|
|
10
|
+
most importantly, we must understand
|
|
11
|
+
- the unique, natural key upon which we can drive idempotency
|
|
12
|
+
- the primary, artificial key upon which we can reference the resource with in foreign keys with other entities
|
|
13
|
+
|
|
14
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/domain/objects/DeclaredStripeCustomer.ts
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { DomainEntity, DomainLiteral } from 'domain-objects';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* .what = a declarative structure which represents a Stripe Customer
|
|
21
|
+
*/
|
|
22
|
+
export interface DeclaredStripeCustomer {
|
|
23
|
+
/**
|
|
24
|
+
* the public stripe customer id of this customer
|
|
25
|
+
*/
|
|
26
|
+
id?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* the email address of the customer
|
|
30
|
+
*
|
|
31
|
+
* note
|
|
32
|
+
* - stripe does not enforce this to be a unique key
|
|
33
|
+
* - however, to create a pit-of-success, this is used as the unique key with declastruct, since it is the only non-id field that we can search on
|
|
34
|
+
*/
|
|
35
|
+
email: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* then name of the customer, if set
|
|
39
|
+
*/
|
|
40
|
+
name: null | string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* a description of the customer, if set
|
|
44
|
+
*/
|
|
45
|
+
description: null | string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* then phone of the customer, if set
|
|
49
|
+
*/
|
|
50
|
+
phone: null | string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* metadata that the customer was tagged with
|
|
54
|
+
*/
|
|
55
|
+
metadata: null | Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class DeclaredStripeCustomer
|
|
59
|
+
extends DomainEntity<DeclaredStripeCustomer>
|
|
60
|
+
implements DeclaredStripeCustomer
|
|
61
|
+
{
|
|
62
|
+
public static primary = ['id'] as const;
|
|
63
|
+
public static unique = ['email'] as const;
|
|
64
|
+
public static nested = {
|
|
65
|
+
metadata: DomainLiteral,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
### 2. next, declare how to translate from the sdk's shape to our declared shape
|
|
72
|
+
|
|
73
|
+
declare how to translate from the sdk's shape to our declared shape
|
|
74
|
+
|
|
75
|
+
why? because its rare that the simplest way to represent a domain-entity is the way that the api has represented it, due to backwards compat && practice differences
|
|
76
|
+
|
|
77
|
+
our objective is
|
|
78
|
+
- make things as simple and intuitive to understand
|
|
79
|
+
- provide a pit of success
|
|
80
|
+
|
|
81
|
+
therefore, we always cast into our own representation, to give ourselves the flexibility to speak more clearly about entities
|
|
82
|
+
|
|
83
|
+
additionally, this enables us to cast them into `domain-object` instances, which give us explicit declarations of the distinct objects and domain-driven features like references
|
|
84
|
+
|
|
85
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/cast/castToDeclaredStripeCustomer.ts
|
|
86
|
+
```ts
|
|
87
|
+
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
88
|
+
import Stripe from 'stripe';
|
|
89
|
+
import { HasMetadata, omit } from 'type-fns';
|
|
90
|
+
|
|
91
|
+
import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
|
|
92
|
+
|
|
93
|
+
export const castToDeclaredStripeCustomer = (
|
|
94
|
+
input: Stripe.Customer,
|
|
95
|
+
): HasMetadata<DeclaredStripeCustomer> => {
|
|
96
|
+
return new DeclaredStripeCustomer({
|
|
97
|
+
id: input.id,
|
|
98
|
+
email:
|
|
99
|
+
input.email ??
|
|
100
|
+
UnexpectedCodePathError.throw(
|
|
101
|
+
'no email found for customer. not a valid declared stripe customer',
|
|
102
|
+
{ input },
|
|
103
|
+
),
|
|
104
|
+
description: input.description ?? null,
|
|
105
|
+
name: input.name ?? null,
|
|
106
|
+
phone: input.phone ?? null,
|
|
107
|
+
metadata: (() => {
|
|
108
|
+
const obj = input.metadata ? omit(input.metadata, ['exid']) : {};
|
|
109
|
+
if (Object.keys(obj).length === 0) return null;
|
|
110
|
+
return obj;
|
|
111
|
+
})(),
|
|
112
|
+
}) as HasMetadata<DeclaredStripeCustomer>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
### 3. next, declare how to get the resource from the remote repository
|
|
119
|
+
|
|
120
|
+
support get.by.unique, get.by.primary, and get.by.ref
|
|
121
|
+
|
|
122
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/getCustomer.ts
|
|
123
|
+
```ts
|
|
124
|
+
import { Ref, RefByPrimary, RefByUnique, isUniqueKeyRef } from 'domain-objects';
|
|
125
|
+
import { BadRequestError, UnexpectedCodePathError } from 'helpful-errors';
|
|
126
|
+
import { HasMetadata, PickOne } from 'type-fns';
|
|
127
|
+
import { VisualogicContext } from 'visualogic';
|
|
128
|
+
|
|
129
|
+
import { StripeApiContext } from '../../domain/constants';
|
|
130
|
+
import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
|
|
131
|
+
import { castToDeclaredStripeCustomer } from '../cast/castToDeclaredStripeCustomer';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* .what = gets a customer from stripe
|
|
135
|
+
*/
|
|
136
|
+
export const getCustomer = async (
|
|
137
|
+
input: {
|
|
138
|
+
by: PickOne<{
|
|
139
|
+
primary: RefByPrimary<typeof DeclaredStripeCustomer>;
|
|
140
|
+
unique: RefByUnique<typeof DeclaredStripeCustomer>;
|
|
141
|
+
ref: Ref<typeof DeclaredStripeCustomer>;
|
|
142
|
+
}>;
|
|
143
|
+
},
|
|
144
|
+
context: StripeApiContext & VisualogicContext,
|
|
145
|
+
): Promise<HasMetadata<DeclaredStripeCustomer> | null> => {
|
|
146
|
+
// handle by ref
|
|
147
|
+
if (input.by.ref)
|
|
148
|
+
return isUniqueKeyRef({ of: DeclaredStripeCustomer })(input.by.ref)
|
|
149
|
+
? getCustomer({ by: { unique: input.by.ref } }, context)
|
|
150
|
+
: getCustomer({ by: { primary: input.by.ref } }, context);
|
|
151
|
+
|
|
152
|
+
// handle get by id
|
|
153
|
+
if (input.by.primary) {
|
|
154
|
+
try {
|
|
155
|
+
const customer = await context.stripe.customers.retrieve(
|
|
156
|
+
input.by.primary.id,
|
|
157
|
+
);
|
|
158
|
+
if (customer.deleted) return null;
|
|
159
|
+
return castToDeclaredStripeCustomer(customer);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!(error instanceof Error)) throw error;
|
|
162
|
+
if (error.message.includes('No such customer')) return null; // handle "null" responses without an error
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// handle get by email
|
|
168
|
+
if (input.by.unique) {
|
|
169
|
+
const {
|
|
170
|
+
data: [customer, ...otherCustomers],
|
|
171
|
+
} = await context.stripe.customers.list({
|
|
172
|
+
email: input.by.unique.email,
|
|
173
|
+
});
|
|
174
|
+
if (otherCustomers.length)
|
|
175
|
+
throw new BadRequestError('more than one customer for this email', {
|
|
176
|
+
input,
|
|
177
|
+
customers: [customer, ...otherCustomers],
|
|
178
|
+
});
|
|
179
|
+
if (!customer) return null;
|
|
180
|
+
return castToDeclaredStripeCustomer(customer);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// otherwise, unexpected input
|
|
184
|
+
throw new UnexpectedCodePathError('invalid input', { input });
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
and dont forget the tests!
|
|
189
|
+
|
|
190
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/getCustomer.integration.test.ts
|
|
191
|
+
```ts
|
|
192
|
+
import { given, then, when, useBeforeAll } from 'test-fns';
|
|
193
|
+
import { HasMetadata } from 'type-fns';
|
|
194
|
+
import { getUuid } from 'uuid-fns';
|
|
195
|
+
|
|
196
|
+
import { getStripeCredentials } from '../../__test_assets__/getStripeCredentials';
|
|
197
|
+
import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
|
|
198
|
+
import { getStripe } from '../auth/getStripe';
|
|
199
|
+
import { getCustomer } from './getCustomer';
|
|
200
|
+
import { setCustomer } from './setCustomer';
|
|
201
|
+
|
|
202
|
+
describe('getCustomer', () => {
|
|
203
|
+
given('a by.primary', () => {
|
|
204
|
+
when('the customer does not exist', () => {
|
|
205
|
+
const stripeCustomerId = getUuid();
|
|
206
|
+
|
|
207
|
+
then('we should get null', async () => {
|
|
208
|
+
const customer = await getCustomer(
|
|
209
|
+
{ by: { primary: { id: stripeCustomerId } } },
|
|
210
|
+
{ stripe: await getStripe(getStripeCredentials()), log: console },
|
|
211
|
+
);
|
|
212
|
+
expect(customer).toEqual(null);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
when('the customer does exist', () => {
|
|
217
|
+
const customerFound: HasMetadata<DeclaredStripeCustomer> = useBeforeAll(
|
|
218
|
+
async () =>
|
|
219
|
+
await setCustomer(
|
|
220
|
+
{
|
|
221
|
+
finsert: {
|
|
222
|
+
email: 'svc-protools@ahbode.dev',
|
|
223
|
+
name: 'svc-protools.test',
|
|
224
|
+
description: 'test',
|
|
225
|
+
metadata: null,
|
|
226
|
+
phone: null,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{ stripe: await getStripe(getStripeCredentials()), log: console },
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
when('we attempt to get the customer', () => {
|
|
234
|
+
then('we should get null', async () => {
|
|
235
|
+
const customer = await getCustomer(
|
|
236
|
+
{ by: { primary: { id: customerFound.id } } },
|
|
237
|
+
{ stripe: await getStripe(getStripeCredentials()), log: console },
|
|
238
|
+
);
|
|
239
|
+
expect(customer?.id).toEqual(customerFound.id);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 4. last, declare how to set the resource into the remote repository
|
|
248
|
+
|
|
249
|
+
support set via both set.finsert and set.upsert idempotent operations
|
|
250
|
+
|
|
251
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/setCustomer.ts
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { serialize } from 'domain-objects';
|
|
255
|
+
import { toHashSha256Sync } from 'hash-fns';
|
|
256
|
+
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
257
|
+
import { HasMetadata, PickOne } from 'type-fns';
|
|
258
|
+
import {
|
|
259
|
+
getResourceNameFromFileName,
|
|
260
|
+
VisualogicContext,
|
|
261
|
+
withLogTrail,
|
|
262
|
+
} from 'visualogic';
|
|
263
|
+
|
|
264
|
+
import { StripeApiContext } from '../../domain/constants';
|
|
265
|
+
import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
|
|
266
|
+
import { castToDeclaredStripeCustomer } from '../cast/castToDeclaredStripeCustomer';
|
|
267
|
+
import { getCustomer } from './getCustomer';
|
|
268
|
+
|
|
269
|
+
export const setCustomer = withLogTrail(
|
|
270
|
+
async (
|
|
271
|
+
input: PickOne<{
|
|
272
|
+
finsert: DeclaredStripeCustomer;
|
|
273
|
+
upsert: DeclaredStripeCustomer;
|
|
274
|
+
}>,
|
|
275
|
+
context: StripeApiContext & VisualogicContext,
|
|
276
|
+
): Promise<HasMetadata<DeclaredStripeCustomer>> => {
|
|
277
|
+
// lookup the customer
|
|
278
|
+
const customerFound = await getCustomer(
|
|
279
|
+
{
|
|
280
|
+
by: {
|
|
281
|
+
unique: {
|
|
282
|
+
email:
|
|
283
|
+
input.finsert?.email ??
|
|
284
|
+
input.upsert?.email ??
|
|
285
|
+
UnexpectedCodePathError.throw('no email in input', { input }),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
context,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// sanity check that if the customer exists, their id matches the user's expectations, if any
|
|
293
|
+
const stripeCustomerIdExpected = input.finsert?.id || input.upsert?.id;
|
|
294
|
+
if (
|
|
295
|
+
customerFound &&
|
|
296
|
+
stripeCustomerIdExpected &&
|
|
297
|
+
stripeCustomerIdExpected !== customerFound.id
|
|
298
|
+
)
|
|
299
|
+
throw new UnexpectedCodePathError(
|
|
300
|
+
'asked to setCustomer with a .primary=id which does not match the .unique=email',
|
|
301
|
+
{
|
|
302
|
+
stripeCustomerIdExpected,
|
|
303
|
+
stripeCustomerIdFound: customerFound.id,
|
|
304
|
+
customerFound,
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// if the customer was found, then handle that
|
|
309
|
+
if (customerFound) {
|
|
310
|
+
// if asked to finsert, then we can return it now
|
|
311
|
+
if (input.finsert) return customerFound;
|
|
312
|
+
|
|
313
|
+
// if asked to upsert, then we can update it now
|
|
314
|
+
if (input.upsert)
|
|
315
|
+
return castToDeclaredStripeCustomer(
|
|
316
|
+
await context.stripe.customers.update(customerFound.id, {
|
|
317
|
+
name: input.upsert.name ?? undefined,
|
|
318
|
+
description: input.upsert.description ?? undefined,
|
|
319
|
+
phone: input.upsert.phone ?? undefined,
|
|
320
|
+
metadata: input.upsert.metadata ?? undefined,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// otherwise, create the customer
|
|
326
|
+
const customerDesired: DeclaredStripeCustomer =
|
|
327
|
+
input.upsert ?? input.finsert;
|
|
328
|
+
const customerCreated = await context.stripe.customers.create(
|
|
329
|
+
{
|
|
330
|
+
email: customerDesired.email,
|
|
331
|
+
name: customerDesired.name ?? undefined,
|
|
332
|
+
description: customerDesired.description ?? undefined,
|
|
333
|
+
phone: customerDesired.phone ?? undefined,
|
|
334
|
+
metadata: customerDesired.metadata ?? undefined,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
idempotencyKey: toHashSha256Sync(
|
|
338
|
+
[
|
|
339
|
+
// stage, // todo: pull stage from context.environment
|
|
340
|
+
'v1.0.0',
|
|
341
|
+
serialize({ ...customerDesired }),
|
|
342
|
+
].join(';'),
|
|
343
|
+
),
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
return castToDeclaredStripeCustomer(customerCreated);
|
|
347
|
+
},
|
|
348
|
+
{ name: getResourceNameFromFileName(__filename) },
|
|
349
|
+
);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
and of course, the tests
|
|
354
|
+
|
|
355
|
+
ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/setCustomer.integration.test.ts
|
|
356
|
+
```ts
|
|
357
|
+
import { given, then, useBeforeAll, when } from 'test-fns';
|
|
358
|
+
import { getUuid } from 'uuid-fns';
|
|
359
|
+
|
|
360
|
+
import { getStripeCredentials } from '../../__test_assets__/getStripeCredentials';
|
|
361
|
+
import { getStripe } from '../auth/getStripe';
|
|
362
|
+
import { setCustomer } from './setCustomer';
|
|
363
|
+
|
|
364
|
+
const log = console;
|
|
365
|
+
|
|
366
|
+
describe('setCustomer', () => {
|
|
367
|
+
given('a .finsert', () => {
|
|
368
|
+
when('the customer does not exist yet', () => {
|
|
369
|
+
const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
|
|
370
|
+
|
|
371
|
+
then('we should create it', async () => {
|
|
372
|
+
const customer = await setCustomer(
|
|
373
|
+
{
|
|
374
|
+
finsert: {
|
|
375
|
+
email,
|
|
376
|
+
name: 'svc-protools.test',
|
|
377
|
+
description: 'test',
|
|
378
|
+
metadata: null,
|
|
379
|
+
phone: null,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
383
|
+
);
|
|
384
|
+
console.log(customer);
|
|
385
|
+
expect(customer.id).toContain('cus_');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
when('the customer already exists', () => {
|
|
390
|
+
const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
|
|
391
|
+
const customerBefore = useBeforeAll(
|
|
392
|
+
async () =>
|
|
393
|
+
await setCustomer(
|
|
394
|
+
{
|
|
395
|
+
finsert: {
|
|
396
|
+
email,
|
|
397
|
+
name: 'svc-protools.test',
|
|
398
|
+
description: 'test',
|
|
399
|
+
metadata: null,
|
|
400
|
+
phone: null,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
then('it should not update the customers attributes', async () => {
|
|
408
|
+
const customerAfter = await setCustomer(
|
|
409
|
+
{
|
|
410
|
+
finsert: {
|
|
411
|
+
email,
|
|
412
|
+
name: 'new name',
|
|
413
|
+
description: 'test',
|
|
414
|
+
metadata: null,
|
|
415
|
+
phone: null,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
419
|
+
);
|
|
420
|
+
expect(customerAfter.name).not.toEqual('new name');
|
|
421
|
+
expect(customerAfter.name).toEqual(customerBefore.name);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
given('a .upsert', () => {
|
|
426
|
+
when('the customer does not exist yet', () => {
|
|
427
|
+
const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
|
|
428
|
+
|
|
429
|
+
then('we should create it', async () => {
|
|
430
|
+
const customer = await setCustomer(
|
|
431
|
+
{
|
|
432
|
+
upsert: {
|
|
433
|
+
email,
|
|
434
|
+
name: 'svc-protools.test',
|
|
435
|
+
description: 'test',
|
|
436
|
+
metadata: null,
|
|
437
|
+
phone: null,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
441
|
+
);
|
|
442
|
+
console.log(customer);
|
|
443
|
+
expect(customer.id).toContain('cus_');
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
when('the customer already exists', () => {
|
|
448
|
+
const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
|
|
449
|
+
const customerBefore = useBeforeAll(
|
|
450
|
+
async () =>
|
|
451
|
+
await setCustomer(
|
|
452
|
+
{
|
|
453
|
+
upsert: {
|
|
454
|
+
email,
|
|
455
|
+
name: 'svc-protools.test',
|
|
456
|
+
description: 'test',
|
|
457
|
+
metadata: null,
|
|
458
|
+
phone: null,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
then('it should update the customers attributes', async () => {
|
|
466
|
+
const customerAfter = await setCustomer(
|
|
467
|
+
{
|
|
468
|
+
upsert: {
|
|
469
|
+
email,
|
|
470
|
+
name: 'new name',
|
|
471
|
+
description: 'test',
|
|
472
|
+
metadata: null,
|
|
473
|
+
phone: null,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{ stripe: await getStripe(getStripeCredentials()), log },
|
|
477
|
+
);
|
|
478
|
+
expect(customerAfter.name).toEqual('new name');
|
|
479
|
+
expect(customerAfter.name).not.toEqual(customerBefore.name);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
we need to be very skeptical of nullable attributes
|
|
2
|
+
|
|
3
|
+
there needs to be a clear domain reason for why an attribute can be null - and it should be clearly articulated
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
if there's no clear reason to support it as null, ensure the attribute has a clear type declared
|
|
8
|
+
|
|
9
|
+
rational or its forbidden
|
|
10
|
+
|
|
11
|
+
===
|
|
12
|
+
|
|
13
|
+
this is a BLOCKER level violation
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
never allow "undefined" attributes for domain objects UNLESS they are database-generated metadata
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
i.e.,
|
|
5
|
+
- ids, createdAt, updatedAt, etc - these are allowed to be undefined, since they're metadata, and only known on read
|
|
6
|
+
|
|
7
|
+
however, everything else should be explicitly knowable
|
|
8
|
+
- if we know that it is null, then set it as null
|
|
9
|
+
- however, we should be really clear as to WHY it can be null; if there is no good domain reason for null, forbid it
|
|
10
|
+
- otherwise, define the exact type
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
=====
|
|
14
|
+
|
|
15
|
+
this is a BLOCKER level violation
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
refs must be immutable
|
|
2
|
+
|
|
3
|
+
primary keys must be immutable; otherwise, they are not a stable identifier => not a reference key candidate
|
|
4
|
+
|
|
5
|
+
unique keys must be immutable; otherwise, they are not a stable identifier => not a reference key candidate
|
|
6
|
+
|
|
7
|
+
===
|
|
8
|
+
|
|
9
|
+
this is a BLOCKER level violation when detected
|