@rt-tools/agent-kit 0.6.0 → 0.8.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 (61) hide show
  1. package/README.md +15 -1
  2. package/assets/checks/check-push-gate.mjs +139 -0
  3. package/assets/checks/check-reuse.mjs +32 -98
  4. package/assets/checks/rt-kit-checks.config.mjs +19 -0
  5. package/assets/checks/signals/angular.json +19 -0
  6. package/assets/checks/signals/core.json +19 -0
  7. package/assets/checks/signals/store.json +14 -0
  8. package/assets/checks/signals/ui-kit-v2.json +48 -0
  9. package/assets/checks/signals/ui-kit.json +11 -0
  10. package/assets/checks/signals/utils.json +15 -0
  11. package/assets/checks/signals.mjs +71 -0
  12. package/assets/defaults/project.sh +9 -7
  13. package/assets/hooks/reuse-first-guard.sh +90 -1
  14. package/assets/patterns/git-workflow-commit.azure.md +18 -0
  15. package/assets/patterns/git-workflow-commit.github.md +18 -0
  16. package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
  17. package/assets/patterns/reuse-first-extend.md +30 -0
  18. package/assets/patterns/spec-driven-domain.md +2 -2
  19. package/assets/rules/git-workflow.azure.md +15 -0
  20. package/assets/rules/git-workflow.github.md +15 -0
  21. package/assets/rules/git-workflow.gitlab.md +15 -0
  22. package/assets/rules/reuse-first.md +31 -13
  23. package/assets/skills/agent-kit.md +13 -8
  24. package/lib/assets.d.ts +1 -1
  25. package/lib/assets.d.ts.map +1 -1
  26. package/lib/assets.js +2 -4
  27. package/lib/assets.js.map +1 -1
  28. package/lib/catalog.d.ts +73 -0
  29. package/lib/catalog.d.ts.map +1 -1
  30. package/lib/catalog.js +121 -0
  31. package/lib/catalog.js.map +1 -1
  32. package/lib/commands.d.ts.map +1 -1
  33. package/lib/commands.js +84 -5
  34. package/lib/commands.js.map +1 -1
  35. package/lib/integrity.d.ts +25 -12
  36. package/lib/integrity.d.ts.map +1 -1
  37. package/lib/integrity.js +40 -18
  38. package/lib/integrity.js.map +1 -1
  39. package/lib/proposals.d.ts +9 -1
  40. package/lib/proposals.d.ts.map +1 -1
  41. package/lib/proposals.js +11 -2
  42. package/lib/proposals.js.map +1 -1
  43. package/lib/retired.d.ts +30 -0
  44. package/lib/retired.d.ts.map +1 -0
  45. package/lib/retired.js +19 -0
  46. package/lib/retired.js.map +1 -0
  47. package/lib/sync.d.ts +45 -1
  48. package/lib/sync.d.ts.map +1 -1
  49. package/lib/sync.js +44 -10
  50. package/lib/sync.js.map +1 -1
  51. package/package.json +1 -1
  52. package/rt-tools-agent-kit-0.8.0.tgz +0 -0
  53. package/assets/laws/application/money.md +0 -41
  54. package/assets/laws/application/ownership.md +0 -32
  55. package/assets/patterns/ownership-scope-resolve.md +0 -69
  56. package/assets/patterns/ownership-session-procedure.md +0 -102
  57. package/assets/patterns/pricing-quote.md +0 -71
  58. package/assets/rules/ownership-scope.md +0 -66
  59. package/assets/rules/ownership-session.md +0 -124
  60. package/assets/rules/pricing.md +0 -68
  61. package/rt-tools-agent-kit-0.6.0.tgz +0 -0
@@ -1,102 +0,0 @@
1
- ---
2
- name: ownership-session-procedure
3
- kind: pattern
4
- rule: ownership-session
5
- description: Паттерн правила ownership-session. Брать, когда процедуре нужна владеющая сущность захода — объявление декоратором, чтение из контекста, отбивка чужой сущности в черновике, контекст в тесте. Не брать для объявления доступа к процедуре — это паттерн permissions-procedure, и не для формы самого класса — это паттерн ts-procedure.
6
- ---
7
-
8
- # Процедура, которой нужна сущность захода
9
-
10
- Паттерн правила `ownership-session`. Что при этом должно быть верно — закон
11
- `docs/constitution/application/ownership.md`.
12
-
13
- ## Когда брать
14
-
15
- - Процедура работает с записями владеющей сущности и должна знать, в какой она работает.
16
- - Процедура читает идентификатор сущности из полей запроса — так больше не делают.
17
- - Пишется тест процедуры, которая сущность захода читает.
18
-
19
- ## Объявление и чтение
20
-
21
- Две строки пишутся вместе: декоратор на классе и чтение из контекста в теле. Перехватчик читает
22
- сущность только для объявивших, поэтому чтение без декоратора отвечает внутренней ошибкой — и
23
- отвечает на первом вызове, а не на сборке.
24
-
25
- ```ts
26
- @Injectable()
27
- @ConnectProcedure()
28
- @RequiresPermission('settings:view')
29
- @RequiresOwner()
30
- export class GetSettingsProcedure implements IConnectProcedure<typeof SettingsContract.method.getSettings> {
31
- public async handle(
32
- _request: GetSettingsRequest,
33
- context: HandlerContext
34
- ): Promise<MessageInitShape<typeof GetSettingsResponseSchema>> {
35
- const settings: IOwnerSettings = await this.#settings.byOwner(ownerOf(context));
36
-
37
- }
38
- }
39
- ```
40
-
41
- Запрос при этом не читается вовсе — отсюда `_request`. Поля сущности в контракте нет: номер и
42
- имя помечены занятыми, а присланный ключ отбивается разбором запроса. Заводить его заново
43
- нельзя: сущность в запросе называет только тот, кто работает поверх всех сразу.
44
-
45
- Декоратор потребности стоит рядом с объявлением доступа, а не вместо него: объявлений доступа у
46
- процедуры по-прежнему ровно одно, и без него приложение не поднимется.
47
-
48
- ## Идентификатор, названный в теле запроса
49
-
50
- Некоторые запросы несут сущность не отдельным полем, а внутри черновика. Расхождение с
51
- сущностью захода отбивается, а не выправляется молча:
52
-
53
- ```ts
54
- const sessionOwnerId: string = ownerOf(context);
55
- if (draft.id && draft.id !== sessionOwnerId) {
56
- throw new ConnectError('owner does not match the session owner', Code.InvalidArgument);
57
- }
58
- ```
59
-
60
- Пустой идентификатор в черновике законен: он приходит пустым, когда админка сохраняет то, чего
61
- ещё не загружала.
62
-
63
- ## Контекст в тесте
64
-
65
- Двойника перехватчика заводить не надо — в контекст кладётся готовое значение:
66
-
67
- ```ts
68
- function context(ownerId: string): HandlerContext {
69
- const values: ContextValues = createContextValues();
70
- values.set(kSessionOwner, ownerId);
71
-
72
- return { values } as unknown as HandlerContext;
73
- }
74
- ```
75
-
76
- Тест на то, что присланный идентификатор не читается, пишется отдельным случаем: запрос несёт
77
- чужую сущность, а спросили у хранилища свою.
78
-
79
- ```ts
80
- it('SC-OWN-54 — читает сущность захода и присланный идентификатор не смотрит', async () => {
81
- const request: GetSettingsRequest = create(GetSettingsRequestSchema, { ownerId: FOREIGN_OWNER });
82
-
83
- await procedure.handle(request, context(SESSION_OWNER));
84
-
85
- expect(asked).toEqual([SESSION_OWNER]);
86
- });
87
- ```
88
-
89
- ## Частые промахи
90
-
91
- - Декоратор забыт, а чтение в теле стоит. Сборка и линт молчат, отказ приходит на первом вызове.
92
- Ловится тестом процедуры, а не проверками дерева.
93
- - Декоратор поставлен вместо объявления доступа. Приложение не поднимется: у процедуры обязано
94
- быть ровно одно объявление доступа, а потребность в сущности к ним не относится.
95
- - Сущность прочитана из запроса «на всякий случай, если контекст пуст». Пустой контекст здесь
96
- означает забытое объявление, и подстановка прячет его до самого прода.
97
- - Процедура читает сущность захода у гостя. Вошедшего у гостевых процедур нет, и сущности в
98
- контексте у них тоже нет: они берут её у записи, с которой пришли, — у страницы, у разговора
99
- по токену, — а когда записи нет вовсе, у единственной сущности хранилища. Второй в хранилище
100
- они отвечают отказом по состоянию.
101
- - Отказ на снятую принадлежность подан как отказ в доступе. Транспорт админки уводит на вход
102
- только по коду неаутентифицированного, а на второй код человек остаётся на закрытом разделе.
@@ -1,71 +0,0 @@
1
- ---
2
- name: pricing-quote
3
- kind: pattern
4
- rule: pricing
5
- description: Паттерн правила pricing. Брать при правке расчёта цены, механик скидки, работы с курсом валют и сумм в письмах и документах — где округлять, как выбирается одна скидка, что кладётся в бронь. Не брать для доступа к процедурам цен — это паттерн permissions-procedure.
6
- ---
7
-
8
- # Расчёт цены
9
-
10
- Паттерн правила `pricing`. Что при этом должно быть верно — закон
11
- `docs/constitution/application/money.md`.
12
-
13
- ## Когда брать
14
-
15
- - Правится расчёт цены или механика скидки.
16
- - В код заходит сумма в чужой валюте.
17
- - Сумма попадает в заказ, письмо, документ или сводку.
18
-
19
- ## Всё считается один раз
20
-
21
- Расчёт живёт в `calculateQuote` и отдаёт готовые числа: ночи, подытог, скидку, итог, признак
22
- минимального срока. Показ, письмо и запись в базу берут одни и те же числа, а не пересчитывают
23
- их каждый у себя.
24
-
25
- ```typescript
26
- const quote: IQuoteResult = calculateQuote({
27
- checkIn,
28
- checkOut,
29
- basePriceThb,
30
- defaultMinNights,
31
- seasons,
32
- lengthDiscounts,
33
- promoCode,
34
- });
35
- ```
36
-
37
- ## Одна скидка — наибольшая
38
-
39
- Механики считаются по отдельности, а выбор между ними один:
40
-
41
- ```typescript
42
- const best: IDiscountCandidate | undefined = bestCandidateOf([
43
- lengthCandidateOf(nights, input, subtotalThb),
44
- promoCandidateOf(input.promoCode, subtotalThb),
45
- ]);
46
- ```
47
-
48
- При равных суммах побеждает промокод: владелец выдал его адресно, а скидка за длительность
49
- досталась бы гостю и без него. Новая механика добавляется третьим кандидатом в тот же вызов, а
50
- не отдельным вычитанием из итога.
51
-
52
- ## Чужая валюта — на показ
53
-
54
- В заказе лежат сумма в валюте хранения и код валюты, в которой гость смотрел цену. Само число в чужой
55
- валюте не хранится нигде:
56
-
57
- ```typescript
58
- export const SUPPORTED_QUOTE_CURRENCIES: readonly string[] = ['USD', 'RUB', 'EUR', 'CNY'];
59
- ```
60
-
61
- Курс тянется по расписанию и кэшируется в хранилище. Курса нет — сумма показывается в валюте хранения без
62
- пересчёта.
63
-
64
- ## Частые промахи
65
-
66
- - Округление на показе: сумма гостя, сумма письма и сумма в базе разойдутся на единицы.
67
- - Сложение двух скидок: владелец таких сумм не закладывал, и объяснить гостю итог нечем.
68
- - Сохранённое число в чужой валюте: оно устареет вместе с курсом.
69
- - Справочная сумма, подписанная как сумма к оплате, — оплата идёт в валюте хранения.
70
- - Письмо в валюте, отличной от той, что гость выбирал на сайте: оно читается как другая цена.
71
- - Дробная часть в сумме: единица хранения целая.
@@ -1,66 +0,0 @@
1
- ---
2
- name: ownership-scope
3
- kind: rule
4
- law: ownership
5
- description: Правило под «Закон о владеющей сущности». Брать, когда процедура или экран работает с записью, принадлежащей владеющей сущности — разрешение идентификатора, умолчание при единственной действующей, сводки по всем. Называет обе формы разрешения и коды отказа. Готовый код — в паттерне ownership-scope-resolve. Чем это названо здесь — в implementation.md рядом.
6
- ---
7
-
8
- # Владеющая сущность — как это устроено здесь
9
-
10
- Правило под закон `docs/constitution/application/ownership.md`. Закон говорит, что должно быть
11
- верно; здесь — каким приёмом это держится. Как владеющая сущность названа в этом дереве и какие
12
- экраны считаются сводками — в `implementation.md` рядом.
13
-
14
- ## Как это называется здесь
15
-
16
- | В законе | Здесь |
17
- | ---------------------------------- | ------------------------------------------------------------------------------- |
18
- | владеющая сущность | запись своего домена; в запросах — поле `<сущность>_id` |
19
- | пустой идентификатор | пустая строка, а не отсутствующее поле: необязательных скаляров в контракте нет |
20
- | сводка | списки и статистика, отвечающие сразу по всем действующим |
21
- | отказ при двух и более действующих | `Code.InvalidArgument` |
22
- | отказ, когда действующих нет вовсе | `Code.FailedPrecondition` |
23
- | ненайденный явный идентификатор | `Code.NotFound` |
24
-
25
- У списков, переведённых на общую выборку, идентификатор отдельным полем запроса не приходит
26
- вовсе: это условие отбора в модели фильтра, а прежний номер поля помечен `reserved`.
27
-
28
- ## Где это лежит
29
-
30
- В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
31
- переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
32
- же дереве, которое держит код иначе.
33
-
34
- ## Как закон применяется здесь
35
-
36
- - **Запись принадлежит владеющей сущности, а не системе.** Общих настроек, действующих сразу
37
- на все, нет: заведение второй не меняет поведение первой.
38
- - **Пустой идентификатор означает единственную действующую.** Пока она одна, клиент вправе её
39
- не называть.
40
- - **При двух и более действующих запрос обязан назвать сущность.** Умолчание перестаёт быть
41
- однозначным, и запрос отбивается как неверный.
42
- - **Названная явно находится, даже если она скрыта.** У скрытой остаются записи, деньги и
43
- переписка.
44
- - **Там, где показана сводка, пустой идентификатор означает все действующие.** Списки и
45
- статистика отвечают по всему набору, а не требуют сперва выбрать одну.
46
- - **Когда действующих нет вовсе, запрос отбивается.** Скрытая вместо отсутствующей действующей
47
- не подставляется.
48
-
49
- ## Паттерны
50
-
51
- - `ownership-scope-resolve` — разрешение сущности в процедуре и обе формы умолчания.
52
-
53
- ## Ловушки
54
-
55
- - **Разрешение разложено на две части намеренно:** чистая функция разбирает случай, обёртка
56
- ходит в хранилище и переводит случай в отказ. Вторая реализация того же правила однажды уже
57
- жила у обработчика статистики соседнего домена и успела разойтись порядком выборки.
58
- - **Отсутствие действующей сущности и ненайденный явный идентификатор — разные случаи для
59
- вызывающего:** первое отвечает `FailedPrecondition`, второе — `NotFound`.
60
- - Таблица владеющих сущностей читается целиком одним запросом — она маленькая, и выборки ей не
61
- нужно.
62
- - **Выбор сущности живёт в общем сторе, а рисует переключатель тот экран, которому он нужен.**
63
- Выбор переживает переход между разделами, но экран без своего переключателя показывает то,
64
- что выбрали на другом, — и это читается как чужие числа в своём разделе. Где переключатель
65
- стоит, названо в именах дерева; снаружи выбора нет вовсе: пользователь приходит на страницу
66
- конкретной сущности.
@@ -1,124 +0,0 @@
1
- ---
2
- name: ownership-session
3
- kind: rule
4
- law: ownership
5
- description: Правило под «Закон о владеющей сущности». Брать при правке процедуры, которой нужна сущность захода, перехватчика доступа и выборок принадлежности — чем названа сущность захода, откуда она берётся, каким условием отбираются записи и какими кодами отвечают отказы. Готовый код — в паттерне ownership-session-procedure. Не брать для разрешения идентификатора в запросе — это правило ownership-scope.
6
- ---
7
-
8
- # Владеющая сущность захода — как это устроено здесь
9
-
10
- Правило под закон `docs/constitution/application/ownership.md`. Закон говорит, что должно быть
11
- верно, когда владеющих сущностей больше одной; здесь — как приложение узнаёт, в какой из них
12
- работает вошедший, и чем держится граница между ними. Разрешение идентификатора, названного в
13
- запросе, — правило `ownership-scope` под тем же законом.
14
-
15
- ## Как это называется здесь
16
-
17
- | В законе | Здесь |
18
- | ---------------------------------------------- | -------------------------------------------------------------------------------- |
19
- | владеющая сущность | запись своего домена; имя — в `implementation.md` рядом |
20
- | принадлежность человека сущности | своя запись на пару «человек и сущность» |
21
- | сущность захода | колонка учётной записи; внутри запроса — ключ контекста, читаемый одной функцией |
22
- | потребность процедуры в сущности захода | декоратор на её классе, стоящий рядом с объявлением доступа |
23
- | отказ вошедшему, у которого принадлежности нет | тот же код, что на неверный пароль, — вошедшего надо увести на вход |
24
- | отказ на чужую сущность, названную в черновике | неверный аргумент |
25
- | забытый декоратор | внутренняя ошибка — отказ приложения, а не вызывающему |
26
-
27
- ## Где это лежит
28
-
29
- В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
30
- переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
31
- же дереве, которое держит код иначе.
32
-
33
- ## Как закон применяется здесь
34
-
35
- - **Сущность захода приезжает в процедуру контекстом запроса, а не полем запроса.** Ключ и его
36
- читатель лежат рядом, как ключ вошедшего и его читатель: вторая копия проверки разошлась бы с
37
- первой в коде отказа.
38
- - **Процедура объявляет потребность декоратором, и чтение без объявления — отказ приложения.**
39
- Править такое нечего ни вызывающему, ни владельцу: это забытое объявление, и оно видно
40
- отказом, а не работой неизвестно в какой сущности.
41
- - **Сущность захода приезжает в контекст только процедурам, объявившим потребность.** Читает её
42
- перехватчик всё равно — тем же обращением, которым читает права вошедшего, — но без
43
- объявления в контекст она не попадает. Потребность стоит рядом с объявлением доступа и счёт
44
- объявлений доступа не меняет.
45
- - **Сущность захода лежит у учётной записи, а не в выданном входе.** Выбор переживает
46
- перезагрузку и не зависит от того, что шлёт клиент; выданный вход говорит только о том, кто
47
- пришёл.
48
- - **Принадлежность человека сущности — своя запись, и в одной сущности она одна.** Две
49
- принадлежности пришлось бы складывать, а результат сложения запретов и разрешений по двум
50
- строкам не прочитать.
51
- - **Человек, у которого принадлежности нет, не входит и не работает: оба отказа те же, что на
52
- неверный пароль.** Принадлежность снимают, пока выданный вход ещё жив, — и тогда отказ
53
- приходит уже посреди работы. Транспорт админки на этот код чистит хранилище и уводит на
54
- страницу входа.
55
- - **Чужая сущность, названная в черновике настроек, отбивается, а не выправляется молча.** Иначе
56
- одним ответом скрывались бы и подделанный запрос, и админка, которая загрузила одну сущность,
57
- а сохраняет в другую.
58
- - **Выборка админки отбирает записи по сущности захода, и снять это условие нечем.** Оно
59
- складывается с условиями отбора клиента, а не заменяется ими: иначе список, поиск и число в
60
- сводке показывают чужое.
61
- - **Запись, названную в запросе своим номером, выборка берёт с условием по сущности, и правка
62
- со снятием идут тем же условием.** Сущность уезжает параметром рядом с идентификатором в то
63
- же условие; у записи без своей колонки граница выводится связью. Чужая запись отвечает так же,
64
- как несуществующая, — и связанный идентификатор сверяется наравне с основным.
65
- - **Выборку по номеру без сущности в условии ловит проверка охвата.** Список точек умер бы со
66
- слиянием: следующая процедура завелась бы без условия, и не заметил бы этого никто. Путь, где
67
- сущности законно нет, назван в списке известного строкой с причиной.
68
- - **Адрес публичной страницы уникален внутри сущности, а не по всей системе.** Пара «сущность и
69
- адрес» стоит уникальным индексом и у записей, и у их прежних адресов; сквозную уникальность
70
- между двумя таблицами держит хранилище — оно читает ту же пару. У прежнего адреса сущность
71
- продублирована колонкой: связью пара в уникальный индекс не собирается.
72
- - **Занятость адреса спрашивается внутри сущности, и чужая занятость владельцу не видна.** Отказ
73
- «адрес занят» на имя, которое держит сосед, выдал бы существование его записи.
74
- - **Публичный вход, нашедший под адресом больше одной записи, отвечает как на
75
- несуществующую.** Гость приходит по адресу и сущности не называет, поэтому признак совпадения
76
- едет отдельным полем результата выборки: пустой ответ забыли бы разобрать, а показ первого
77
- попавшегося отдал бы гостю чужую запись молча. Владелец узнаёт о таком случае строкой лога,
78
- заведённой в списке отбираемых.
79
- - **Принадлежность записи сущности записана колонкой везде, где её не из чего вывести.** Там,
80
- где у записи обязательна связь с другой записью сущности, граница выводится связью; у визита,
81
- события и разговора выводить её не из чего — заход бывает записан на несуществующий путь, а
82
- разговор начат с корня сайта.
83
-
84
- ## Чего из закона здесь нет
85
-
86
- Внутри сущности границ нет: право открывает раздел целиком, и сотрудник, которому доверена одна
87
- запись, видит все записи своей сущности.
88
-
89
- Признака, по которому гость назвал бы сущность, может не быть вовсе — тогда там, где гость не
90
- назвал ничего, читатель берёт единственную сущность хранилища и отвечает отказом по состоянию,
91
- когда их больше одной. Тем же вызвана и временная граница у адреса страницы: совпавший адрес
92
- недоступен обеим сущностям, потому что выбрать между ними нечем.
93
-
94
- Роль живёт у учётной записи, а не у принадлежности, пока принадлежность у человека одна:
95
- колонки прав у принадлежности стоят и не читаются, а права читаются у учётной записи при каждом
96
- вызове.
97
-
98
- Выбора сущности сотрудником может не быть: сущностью захода остаётся то, что записано у учётной
99
- записи, и сменить это значение нечем, пока нет ни селектора, ни процедуры. Тогда выбором
100
- остаётся единственная принадлежность.
101
-
102
- ## Паттерны
103
-
104
- - `ownership-session-procedure` — процедура, которой нужна сущность захода: объявление, чтение,
105
- отказы, контекст в тесте.
106
-
107
- ## Ловушки
108
-
109
- - **Сущность захода и принадлежность — разные вещи.** Колонка учётной записи говорит, в какой
110
- сущности человек работает сейчас; принадлежность — в каких он вправе работать вообще.
111
- Значение колонки без принадлежности к той же сущности ничего не значит: выборка сверяет их
112
- между собой и отдаёт пусто, когда они разошлись.
113
- - **Забытый декоратор не ловится ни сборкой, ни линтером.** Процедура, читающая сущность захода
114
- без объявления потребности, собирается и проходит проверки — отказ приходит на первом же
115
- вызове. Пишутся эти две строки вместе, а покрывается это тестом процедуры.
116
- - **Потребность в сущности доступом не является.** Объявлений доступа у процедуры по-прежнему
117
- ровно одно, и декоратор потребности к этому счёту не относится: сущность нужна и процедуре с
118
- правом, и процедуре, открытой любому вошедшему.
119
- - **Отказ снятой принадлежности — тот же, что на неверный пароль, а не отказ в доступе.**
120
- Второй оставил бы человека на закрытом разделе с непонятной причиной и без выхода, хотя
121
- лечится это только новым входом.
122
- - **Снятое поле контракта не заводится заново под тем же номером.** Номер и имя помечены как
123
- занятые, и метка снимается только вместе с решением отдать номер новому полю, — а такое
124
- решение ломает уже выкаченного клиента молча: он прочитает чужое значение как своё.
@@ -1,68 +0,0 @@
1
- ---
2
- name: pricing
3
- kind: rule
4
- law: money
5
- description: Правило под «Закон о деньгах». Брать при правке расчёта цены, скидок, курсов валют, сумм в заказах, письмах и сводках. Называет хранение в целых единицах валюты хранения, единственное округление, выбор одной наибольшей скидки и справочный пересчёт. Готовый код — в паттерне pricing-quote. Чем это названо здесь — в implementation.md рядом.
6
- ---
7
-
8
- # Суммы — как это устроено здесь
9
-
10
- Правило под закон `docs/constitution/application/money.md`. Закон говорит, что должно быть
11
- верно; здесь — каким приёмом это держится. Валюта хранения, набор справочных валют и имена
12
- колонок — при этом дереве, в `implementation.md` рядом.
13
-
14
- ## Как это называется здесь
15
-
16
- | В законе | Здесь |
17
- | ------------- | ---------------------------------------------------------------------------------------------- |
18
- | сумма | целое число единиц валюты хранения; код валюты стоит в имени поля — `total<Код>`, `price<Код>` |
19
- | валюта показа | поле заказа с кодом валюты, в которой пользователь смотрел цену |
20
- | расчёт цены | одна чистая функция: на входе — что заказано, на выходе — подытог, скидка и итог |
21
- | скидка | механики перечислены в `implementation.md`; правило про выбор одно на все |
22
- | курс | кэш в хранилище, обновляется по расписанию |
23
-
24
- ## Где это лежит
25
-
26
- В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
27
- переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
28
- же дереве, которое держит код иначе.
29
-
30
- ## Как закон применяется здесь
31
-
32
- - **Сумма хранится в целых единицах валюты хранения.** Дробная часть даёт расхождение между
33
- подытогом и итогом: показанное построчно перестаёт складываться в показанное внизу. Если
34
- дробная часть в предметной области значима, единицей хранения становится она сама.
35
- - **Код валюты стоит в имени поля, а не подразумевается.** Поле `total` без кода читается как
36
- «сумма вообще», и первый же пересчёт кладёт в него чужую валюту.
37
- - **Округление происходит один раз — при расчёте цены.** Сумма на экране, сумма в письме и
38
- сумма в хранилище — одно и то же число, а не три результата одного пересчёта.
39
- - **Из подходящих скидок применяется одна — наибольшая.** Сравниваются они в валюте хранения:
40
- доля и сумма иначе несравнимы.
41
- - **При равной выгоде побеждает та скидка, которую пользователь ввёл руками.** Он ждёт
42
- подтверждения своему действию, а «код не сработал» при той же итоговой цене читается как
43
- поломка.
44
- - **Скидка суммой ограничена подытогом.** Иначе она увела бы цену ниже нуля.
45
- - **Пересчёт в чужую валюту не хранится.** В заказе лежат сумма в валюте хранения и код валюты
46
- показа; само число вычисляется на показ.
47
- - **Оплата идёт в валюте хранения, остальные валюты — справка.** Ни подтверждение, ни документ
48
- не называют справочное число суммой к оплате.
49
- - **Валюта показа следует за пользователем.** Он выбрал её на экране — в письме сумма
50
- пересчитана в неё же.
51
- - **Курс недоступен — сумма показывается в валюте хранения без пересчёта.** Пересчёт по
52
- неизвестно какому курсу хуже отсутствующего: отсутствие видно, а неверный курс — нет.
53
-
54
- ## Паттерны
55
-
56
- - `pricing-quote` — расчёт цены, выбор скидки, работа с курсом.
57
-
58
- ## Ловушки
59
-
60
- - **Курс тянется по расписанию и кэшируется.** Само число в чужой валюте нигде не сохраняется.
61
- - **Дробная сумма в контракте — признак того, что округление уехало на показ.** Округление
62
- одно, и живёт оно в расчёте.
63
- - **Справочная сумма рядом с суммой к оплате читается как цена.** В письме и в документе
64
- справочное число подписывается как справка.
65
- - **Набор валют переключателя бывает шире набора курсов.** Набор переключателя собирается из
66
- начальных валют локалей, и валюты, курса для которых никто не тянет, попадают в него сами:
67
- гость такой локали получает отказ курса, ничего не выбирая. Два набора сверяются между собой,
68
- а расхождение чинится задачей — правкой текста оно не лечится.
Binary file