feat: bonus program registration#135
Conversation
WalkthroughAdds client profile routing and UI, introduces a bonus program registration flow with form, server API, DB fields/repo method, and store updates. Centralizes UDrawer UI via app config. Adds confetti and gyroscope shared composables, integrates confetti into layout and form success. Adjusts navigation and initialization guards. Updates TypeScript version. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant C as Client UI (Points/Registration)
participant F as Form (CompleteBonusProgramRegistration)
participant A as API /api/auth/bonus
participant R as Repo Client.updateBonusProgram
participant S as Store (useClientStore)
participant CF as Confetti (useConfetti)
U->>C: Tap "Активировать"
C->>C: Open drawer
U->>F: Submit form (name, surname?, email, birthDate)
F->>A: POST body + auth
A->>R: updateBonusProgram(id, data)
R-->>A: Updated client
A-->>F: { ok: true }
F->>S: S.update()
F->>CF: pop()
F->>C: emit success / close drawer
rect rgba(200,255,200,0.2)
note right of CF: New shared confetti shown\nfor ~5s overlay
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/storefront-telegram/app/components/Navigation.vue (1)
35-39: Update stale route in atrium-telegram and localize all titles
- In apps/storefront-telegram/app/components/Navigation.vue (lines 31 & 37), replace hard-coded titles with i18n calls:
- title: 'Кабинет', + title: t('nav.cabinet'), - title: 'Меню', + title: t('nav.menu'),- In apps/atrium-telegram/app/components/Navigation.vue, the route still uses
path: '/secret1'(line 53) andtitle: 'Секрет'; rename the path to/navigationand replace the hard-coded title witht('nav.secret').apps/storefront-telegram/app/components/client/PointsCard.vue (1)
107-114: Add rel to external links opened in a new tab.Prevent
window.openerleaks.- <ULink to="https://sushi-love.ru" target="_blank"> + <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer"> условиями программы лояльности, </ULink> - <ULink to="https://sushi-love.ru" target="_blank"> + <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer"> политикой конфиденциальности </ULink> и <ULink to="https://sushi-love.ru" target="_blank"> условиями обработки персональных данных. </ULink>
🧹 Nitpick comments (22)
apps/storefront-telegram/app/composables/useConfetti.ts (1)
1-15: Make pops idempotent and clear the timeout on scope dispose.Prevents multiple timers from fighting and cleans up when scope ends.
Apply:
function _useConfetti() { const isShown = ref(false) + let t: ReturnType<typeof setTimeout> | undefined - function pop() { - isShown.value = true - setTimeout(() => { - isShown.value = false - }, 5000) - } + function pop(durationMs = 5000) { + if (t) clearTimeout(t) + isShown.value = true + t = setTimeout(() => { + isShown.value = false + t = undefined + }, durationMs) + } + + tryOnScopeDispose(() => { + if (t) clearTimeout(t) + }) return { isShown, pop } }apps/storefront-telegram/i18n/locales/ru-RU.json (1)
9-15: Localize “email” and confirm new keys exist in all locales
- Suggest translating “Email” to “Электронная почта” for consistency with Russian UI text.
- Please verify that
common.surname,common.email, andcommon.birth-dateare added to other locales to avoid runtime fallbacks.Apply:
- "email": "Email", + "email": "Электронная почта",apps/storefront-telegram/app/composables/useGyroscope.ts (1)
1-3: Add explicit imports for Vue/VueUse utilities to prevent build-time issuesIf auto-imports are not enabled for TS files,
ref,onMounted,onUnmounted, andcreateSharedComposablewill be undefined at compile time. Add explicit imports.import type { EventListener } from '@telegram-apps/sdk-vue' import { off, on } from '@telegram-apps/sdk-vue' +import { ref, onMounted, onUnmounted } from 'vue' +import { createSharedComposable } from '@vueuse/core'packages/database/src/tables.ts (1)
604-611: Add basic data integrity for new client fields (email/birthDate) and confirm uniqueness strategy
- Consider enforcing a reasonable
birth_daterange (not in the future, >= a sane minimum like 1900-01-01) and validating email format server-side (and/or add a functional unique index onlower(email)if you want per-client uniqueness, case-insensitive).- If duplicates are intentionally allowed across clients, ignore the unique index suggestion but please confirm.
apps/storefront-telegram/app/components/NavigationButton.vue (1)
53-55: Avoid history stack bloat when returning from nested client pages.Consider using
router.replace('/client')whencanReturnis true so Back won’t take users back into the nested page.Example change outside this hunk:
- function handleRedirect(path: string) { - vibrate() - router.push(path) - } + function handleRedirect(path: string, { replace = false } = {}) { + vibrate() + return replace ? router.replace(path) : router.push(path) +}Then in the template:
- @click="canScrollToTop ? handleScrollToTop() : handleRedirect(route.path)" + @click="canScrollToTop ? handleScrollToTop() : handleRedirect(canReturn ? '/client' : route.path, { replace: canReturn })"apps/storefront-telegram/app/layouts/default.vue (1)
2-2: Confirm confetti overlay doesn’t block UI interactions.Ensure the root of
BackgroundConfettihaspointer-events-none(or equivalent) and an appropriatez-indexso bottom navigation and city selector remain clickable.I can review
components/background/Confetti.vueand propose a minimal diff if needed.apps/storefront-telegram/app/pages/navigation.vue (1)
1-5: Localize the placeholder heading.Hardcoded Russian text bypasses i18n. Use
t()to enable translations.- <PageContainer> - <h1>Еще рано сюда заходить</h1> - </PageContainer> + <PageContainer> + <h1>{{ $t('navigation.comingSoon') }}</h1> + </PageContainer>If the key doesn’t exist, I can add it to locales.
apps/storefront-telegram/app/components/background/Confetti.vue (1)
3-3: Avoid hardcoded stage dimensions; let it fit the viewport.Static 900×1200 can clip on many devices. Either omit stageWidth/Height (if the directive auto-sizes) or bind them reactively to the window size on client.
Example minimal change if auto-size is supported:
- <div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" /> + <div v-confetti="{ particleCount: 240, duration: 4500, force: 0.4 }" />apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue (2)
8-9: Use proper currency formatting.Concatenating a sign can misformat spacing/decimals. Prefer Intl currency format (using a currency code from the channel store, if available).
- Закажите еще на {{ Intl.NumberFormat('ru').format(clientStore.nextLevelAmount) }} {{ channelStore.currencySign }} + Закажите еще на + {{ + Intl.NumberFormat('ru-RU', { style: 'currency', currency: channelStore.currencyCode || 'RUB' }) + .format(clientStore.nextLevelAmount) + }}
5-9: Externalize user-facing strings to i18n.Move hardcoded RU strings to locale files for consistency with the rest of the app.
apps/storefront-telegram/server/api/auth/bonus.post.ts (2)
1-11: Email and birthDate validation appear too lax upstream.The shared schema snippet validates only string length; add an email pattern and ISO date check to prevent bad data.
Follow-up in apps/storefront-telegram/shared/services/client.ts (not in this diff):
export const completeBonusProgramRegistrationSchema = type({ name: type('2 <= string <= 50').describe('error.length.invalid'), surname: type('2 <= string <= 50 | undefined').optional(), email: type('email & string <= 80').describe('error.email.invalid'), birthDate: type('string & /^(\\d{4})-(\\d{2})-(\\d{2})$/').describe('error.date.invalid'), })(Adjust for arktype’s exact syntax you use.)
1-23: Log hygiene for PII.Ensure any centralized errorResolver or request logging excludes name/email/birthDate from logs.
apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue (3)
80-83: Open the drawer deterministically.Avoid toggling to prevent accidental close on rapid taps.
-function handleClick() { - vibrate() - isDrawerOpened.value = !isDrawerOpened.value -} +function handleClick() { + vibrate() + isDrawerOpened.value = true +}
59-69: External links: add rel for safety.For target="_blank", include rel="noopener noreferrer".
- <ULink to="https://sushi-love.ru" target="_blank"> + <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer"> ... - <ULink to="https://sushi-love.ru" target="_blank"> + <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer"> ... - </ULink> и <ULink to="https://sushi-love.ru" target="_blank"> + </ULink> и <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">
4-10: Localize strings.Move RU strings to i18n for consistency.
apps/storefront-telegram/app/pages/client/index.vue (1)
4-9: Localize header/labels.Move hardcoded RU strings to i18n.
apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue (3)
31-41: Use proper email input semantics.Improve UX/validation on mobile by using
type="email"and autocomplete.<UInput v-model="state.email" + type="email" + inputmode="email" + autocomplete="email" size="xl" class="w-full" />
48-54: Date input: add autocomplete.Small UX boost for some browsers.
<UInput v-model="state.birthDate" type="date" + autocomplete="bday" size="xl" class="w-full" />
63-64: Localize the submit label.Use
$t(...)for consistency with other labels.apps/storefront-telegram/app/components/client/PointsCard.vue (3)
54-55: Lazy-load registration/progress components.Reduce initial payload with Nuxt’s lazy components.
- <ClientBonusProgramRegistration v-if="!clientStore.isBonusProgramParticipant" /> - <ClientLevelIncreaseProgress v-else /> + <LazyClientBonusProgramRegistration v-if="!clientStore.isBonusProgramParticipant" /> + <LazyClientLevelIncreaseProgress v-else />
7-8: Clamp gyro rotation to avoid extreme transforms.A noisy sensor can yield jerky rotations. Clamp in style or inside
useGyroscope().Also applies to: 14-15
86-88: Copy nit: “всё время”.Minor Russian grammar tweak: “за всё время”.
- Кешбэк определяется по сумме заказов за все время. Система включает уровни + Кешбэк определяется по сумме заказов за всё время. Система включает уровни
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (24)
apps/storefront-telegram/app/app.config.ts(1 hunks)apps/storefront-telegram/app/components/CitySelector.vue(0 hunks)apps/storefront-telegram/app/components/Navigation.vue(1 hunks)apps/storefront-telegram/app/components/NavigationButton.vue(2 hunks)apps/storefront-telegram/app/components/background/Confetti.vue(1 hunks)apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue(1 hunks)apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue(1 hunks)apps/storefront-telegram/app/components/client/PointsCard.vue(3 hunks)apps/storefront-telegram/app/components/client/PointsCardBottomBlock.vue(1 hunks)apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue(1 hunks)apps/storefront-telegram/app/composables/useConfetti.ts(1 hunks)apps/storefront-telegram/app/composables/useGyroscope.ts(1 hunks)apps/storefront-telegram/app/layouts/default.vue(1 hunks)apps/storefront-telegram/app/pages/client/index.vue(1 hunks)apps/storefront-telegram/app/pages/navigation.vue(1 hunks)apps/storefront-telegram/app/pages/user.vue(0 hunks)apps/storefront-telegram/app/stores/client.ts(4 hunks)apps/storefront-telegram/app/utils/init.ts(2 hunks)apps/storefront-telegram/i18n/locales/ru-RU.json(1 hunks)apps/storefront-telegram/server/api/auth/bonus.post.ts(1 hunks)apps/storefront-telegram/shared/services/client.ts(1 hunks)packages/database/src/repository/client.ts(1 hunks)packages/database/src/tables.ts(1 hunks)pnpm-workspace.yaml(1 hunks)
💤 Files with no reviewable changes (2)
- apps/storefront-telegram/app/pages/user.vue
- apps/storefront-telegram/app/components/CitySelector.vue
🧰 Additional context used
🧬 Code graph analysis (2)
packages/database/src/repository/client.ts (2)
packages/database/src/types.ts (1)
ClientDraft(146-146)packages/database/src/tables.ts (1)
clients(599-617)
apps/storefront-telegram/server/api/auth/bonus.post.ts (1)
apps/storefront-telegram/shared/services/client.ts (1)
completeBonusProgramRegistrationSchema(8-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (14)
pnpm-workspace.yaml (1)
69-69: No TS pin conflicts detected—confirm toolchain compatibility. All packages now reference TypeScript ^5.9.2 via the workspace catalog with no local overrides. Run a full CI typecheck/build to ensure Nuxt 4, @vitejs/plugin-vue 6, and vue-tsc 3 all remain green.apps/storefront-telegram/app/app.config.ts (1)
54-58: Use a valid Tailwind arbitrary‐value utility for drawer max-height
max-h-10/12isn’t provided by default and will fail to compile. Replace it with an arbitrary‐value class, for example:- content: '!max-h-10/12', + // Cap drawer content height to avoid viewport overflow on mobile + content: '!max-h-[80vh]',Also verify that
contentis the correct slot name for UDrawer in your installed @nuxt/ui version.apps/storefront-telegram/app/components/client/PointsCardBottomBlock.vue (1)
1-7: LGTM: clean slot wrapper.Simple, reusable container with sensible defaults.
apps/storefront-telegram/app/utils/init.ts (1)
14-15: Granular feature gating looks good; double-check gyroscope availability/rate
- Nice move to per-feature
.ifAvailable()checks and addingmountSwipeBehavior.- Consider confirming that
web_app_start_gyroscopeis supported on your minimum Telegram client versions and thatrefresh_rate: 80is acceptable; some environments throttle or cap rates.Also applies to: 80-85, 87-89, 95-98
apps/storefront-telegram/app/components/NavigationButton.vue (2)
17-21: Good UX: return affordance on nested client routes.The conditional undo icon correctly signals “return to /client” when nested. Click behavior routes to
route.pathanyway, so the UX aligns.
50-52: Safer scroll-to-top gating.Requiring exact
'/'for both button and current route prevents false positives. Looks good.apps/storefront-telegram/app/stores/client.ts (3)
23-24: New bonus-program flags wired correctly.
bonusProgramParticipantFrom+isBonusProgramParticipantare straightforward and composable-friendly.
85-85: API hydration includes bonus flag.Assigning
bonusProgramParticipantFromfrom/api/auth/meresponse is correct.
217-219: Store exposure is consistent.Exposing the new refs keeps the public API cohesive.
apps/storefront-telegram/app/components/Navigation.vue (1)
29-31: No remaining references to the old ‘user’ route — project-wide search across .vue/.js/.ts forname: 'user',path: '/user',to="/user", androuter.push|replace({ name: 'user' })returned zero matches.apps/storefront-telegram/app/components/background/Confetti.vue (1)
2-2: z-index summary: Confetti.vue usesz-40, the same as Modal.vue, and sits beneath Navigation.vue’sz-50.
If confetti must appear above modals or the bottom nav, update toz-50(or extend Tailwind for a higher value likez-[60]).apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue (1)
2-2: Check 0-threshold case.v-if hides the block when nextLevelAmount is 0. If “exactly at threshold” should still show the progress (or a different state), handle 0 explicitly.
apps/storefront-telegram/app/pages/client/index.vue (1)
47-50: Revisit the “Select city” action.Calling updateCity(null) clears the city rather than opening selection. If there’s a CitySelector flow, hook into it (e.g., open a drawer or navigate).
apps/storefront-telegram/app/components/client/PointsCard.vue (1)
123-123: Confirm SSR-safety of useGyroscope.Ensure the composable guards for
process.client/onMountedand degrades gracefully when sensors are unavailable.
| <div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none"> | ||
| <div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" /> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Don’t block interactions; make overlay non-interactive and hidden from a11y.
Add pointer-events-none and aria-hidden so the confetti layer doesn’t steal taps/clicks and is ignored by screen readers.
Apply:
- <div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none">
+ <div
+ v-if="isShown"
+ class="z-40 pointer-events-none mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none"
+ aria-hidden="true"
+ >📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none"> | |
| <div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" /> | |
| </div> | |
| <div | |
| v-if="isShown" | |
| class="z-40 pointer-events-none mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none" | |
| aria-hidden="true" | |
| > | |
| <div | |
| v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/background/Confetti.vue around lines
2-4 the confetti overlay currently blocks user interactions and is announced to
assistive tech; add the utility class pointer-events-none to the outer div and
set aria-hidden="true" (and optionally role="presentation") on the same element
so the layer is non-interactive and hidden from screen readers while preserving
visual effect.
| <FormCompleteBonusProgramRegistration | ||
| @submitted="handleClose()" | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Close the drawer only on successful submission.
If submitted fires before success, the drawer may close on failures. Prefer a success/completed event.
- <FormCompleteBonusProgramRegistration
- @submitted="handleClose()"
- />
+ <FormCompleteBonusProgramRegistration
+ @success="handleClose()"
+ />(If the child emits only submitted, emit success there upon API ok.)
🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue
around lines 55-57, the parent currently closes the drawer on the child's
"submitted" event which may fire before the operation succeeds; change the
parent to close the drawer only when the child emits a success/completed event
(e.g., "success" or "completed") and keep the child emitting "submitted" for
attempt and emit the new success event only after the API call returns OK; if
the child does not yet emit such an event, update the child to emit a
success/completed event on successful API response and update the parent to
listen for that event instead of "submitted".
| v-model="clientStore.nextLevelProgressPercent" | ||
| color="primary" | ||
| :ui="{ | ||
| base: 'bg-primary/10', | ||
| }" | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Don’t use v-model for a read-only progress bar.
Bind the value one-way to avoid accidental store mutations and make intent clear.
- <UProgress
- v-model="clientStore.nextLevelProgressPercent"
+ <UProgress
+ :model-value="clientStore.nextLevelProgressPercent"
color="primary"
:ui="{
base: 'bg-primary/10',
}"
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| v-model="clientStore.nextLevelProgressPercent" | |
| color="primary" | |
| :ui="{ | |
| base: 'bg-primary/10', | |
| }" | |
| /> | |
| <UProgress | |
| :model-value="clientStore.nextLevelProgressPercent" | |
| color="primary" | |
| :ui="{ | |
| base: 'bg-primary/10', | |
| }" | |
| /> |
🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue
around lines 13–18, the progress bar uses v-model which two-way binds to
clientStore.nextLevelProgressPercent; change this to a one-way binding to avoid
accidental store mutations — replace v-model with a read-only prop (e.g.,
:value="clientStore.nextLevelProgressPercent" or
:model-value="clientStore.nextLevelProgressPercent" depending on the progress
component API) and remove any accompanying @update or input handlers so the
component only reads the store value.
| :validate="createValidator(completeBonusProgramRegistrationSchema)" | ||
| :state="state" |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
Import createValidator (avoid runtime undefined).
Unless auto-imported, createValidator will be undefined in the template. Import it explicitly.
Apply:
<script setup lang="ts">
import type { CompleteBonusProgramRegistration } from '#shared/services/client'
import type { FormSubmitEvent } from '@nuxt/ui'
+import { createValidator } from '@nuxt/ui'
import { completeBonusProgramRegistrationSchema } from '#shared/services/client'Also applies to: 72-76
Import createValidator from @nuxt/ui to avoid runtime undefined.
Add alongside the existing imports in <script setup> (e.g. import { createValidator } from '@nuxt/ui'), and repeat for the identical usage at lines 72–76.
🤖 Prompt for AI Agents
In
apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue
around lines 3–4 and again where an identical usage appears at lines 72–76, the
template uses createValidator but it is not imported which will cause a runtime
undefined; add an import for createValidator from '@nuxt/ui' in the <script
setup> imports (e.g. import { createValidator } from '@nuxt/ui') so the function
is available for both template bindings.
| <UButton | ||
| type="submit" | ||
| variant="solid" | ||
| color="secondary" | ||
| size="xl" | ||
| trailing-icon="i-lucide-flag" | ||
| block | ||
| label="Завершить регистрацию" | ||
| class="mt-3" | ||
| :ui="{ | ||
| trailingIcon: 'ms-0', | ||
| }" | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Prevent double-submit and handle missing auth defensively.
Add a pending flag to disable the button and guard when initDataRaw is absent. Also remove unnecessary Promise.all.
@@
- <UButton
+ <UButton
type="submit"
variant="solid"
color="secondary"
size="xl"
trailing-icon="i-lucide-flag"
block
label="Завершить регистрацию"
class="mt-3"
+ :loading="pending"
:ui="{
trailingIcon: 'ms-0',
}"
/>
@@
-const state = ref<Partial<CompleteBonusProgramRegistration>>({
+const state = ref<Partial<CompleteBonusProgramRegistration>>({
name: clientStore.name,
surname: clientStore.surname,
email: undefined,
birthDate: undefined,
})
+const pending = ref(false)
@@
async function onSubmit(event: FormSubmitEvent<CompleteBonusProgramRegistration>) {
emit('submitted')
try {
+ if (!clientStore.initDataRaw) {
+ vibrate('error')
+ return
+ }
+ pending.value = true
await $fetch('/api/auth/bonus', {
method: 'POST',
headers: {
Authorization: `tma ${clientStore.initDataRaw}`,
},
body: event.data,
})
- await Promise.all([
- clientStore.update(),
- ])
+ await clientStore.update()
vibrate('success')
pop()
emit('success')
} catch (error) {
console.error(error)
vibrate('error')
+ } finally {
+ pending.value = false
}
}Also applies to: 83-88, 90-113
🤖 Prompt for AI Agents
In
apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue
around lines 56-68 (also apply same changes to 83-88 and 90-113), prevent
double-submit and handle missing auth defensively: add a reactive pending flag
(e.g., ref(false)) and bind it to the UButton disabled prop (or use
:disabled="pending") so the button is disabled while submission is in progress;
at the start of the submit handler return early if pending is true or if
initDataRaw is falsy (guard against missing auth/data); set pending = true
before async work and ensure pending = false in a finally block; replace the
unnecessary Promise.all wrapper with direct awaits for sequential/independent
calls as appropriate to your logic, and ensure any error paths log/handle errors
while still clearing pending.
| const body = await readBody(event) | ||
| const data = completeBonusProgramRegistrationSchema(body) | ||
| if (data instanceof type.errors) { | ||
| throw data | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Strengthen validation flow with explicit guards.
Ensure body is an object and surface validation errors with a 400 code. Current arktype instanceof check may be brittle across versions.
- const body = await readBody(event)
- const data = completeBonusProgramRegistrationSchema(body)
- if (data instanceof type.errors) {
- throw data
- }
+ const body = await readBody(event)
+ if (!body || typeof body !== 'object') {
+ throw createError({ statusCode: 400, statusMessage: 'Invalid body' })
+ }
+ const data = completeBonusProgramRegistrationSchema(body)
+ if (data instanceof type.errors) {
+ throw createError({ statusCode: 400, statusMessage: 'Validation error', data })
+ }(If you keep errorResolver, pass data into it.)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const body = await readBody(event) | |
| const data = completeBonusProgramRegistrationSchema(body) | |
| if (data instanceof type.errors) { | |
| throw data | |
| } | |
| const body = await readBody(event) | |
| if (!body || typeof body !== 'object') { | |
| throw createError({ statusCode: 400, statusMessage: 'Invalid body' }) | |
| } | |
| const data = completeBonusProgramRegistrationSchema(body) | |
| if (data instanceof type.errors) { | |
| throw createError({ statusCode: 400, statusMessage: 'Validation error', data }) | |
| } |
🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 7 to 11,
the current validation uses an instanceof check on arktype errors which is
brittle and doesn't ensure the request body is an object or return a proper 400;
update the flow to first assert that body is a non-null object before calling
completeBonusProgramRegistrationSchema, then handle validation failures by
detecting the schema error shape (avoid instanceof) and throw a HTTP 400 error
with the validation details (or call errorResolver with the validation result
passed in if you keep errorResolver). Ensure the thrown/returned error includes
a statusCode 400 and the schema error payload so clients receive clear
validation feedback.
| await repository.client.updateBonusProgram(event.context.client.id, { | ||
| ...data, | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add an auth guard for event.context.client.
Defensive check avoids crashes when context is missing and returns 401.
- await repository.client.updateBonusProgram(event.context.client.id, {
+ const clientId = event.context.client?.id
+ if (!clientId) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+ await repository.client.updateBonusProgram(clientId, {
...data,
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await repository.client.updateBonusProgram(event.context.client.id, { | |
| ...data, | |
| }) | |
| const clientId = event.context.client?.id | |
| if (!clientId) { | |
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) | |
| } | |
| await repository.client.updateBonusProgram(clientId, { | |
| ...data, | |
| }) |
🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 13–15,
add a defensive auth guard that checks event.context.client before calling
repository.client.updateBonusProgram; if event.context.client is missing or
falsy, return an HTTP 401 Unauthorized response (early return) instead of
proceeding, e.g. perform the guard at the top of the handler and use the
project's standard error/response helper to send a 401 and stop execution.
| throw errorResolver(error) | ||
| } |
There was a problem hiding this comment.
Missing import for errorResolver causes runtime/compile failure.
errorResolver is undefined here. Either import it or use createError/sendError.
Option A — import your helper (adjust path as in your project):
-import { completeBonusProgramRegistrationSchema } from '#shared/services/client'
+import { completeBonusProgramRegistrationSchema } from '#shared/services/client'
+import { errorResolver } from '#server/utils/errorResolver' // <-- verify pathOption B — replace with h3 createError:
- } catch (error) {
- throw errorResolver(error)
- }
+ } catch (error) {
+ throw createError({ statusCode: 400, statusMessage: 'Bad Request', data: error })
+ }If using Option B, add:
+import { createError } from 'h3'🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 21-22,
errorResolver is referenced but not imported causing a compile/runtime failure;
fix by either importing errorResolver from the correct helper path used in the
project (add the proper import statement at the top of the file) or replace the
call with a framework error utility (e.g., use h3's createError or the project
sendError helper), making sure to return or throw the created error consistently
with surrounding handlers.
| export const completeBonusProgramRegistrationSchema = type({ | ||
| name: type('2 <= string <= 50').describe('error.length.invalid'), | ||
| surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(), | ||
| email: type('2 <= string <= 80').describe('error.length.invalid'), | ||
| birthDate: type('string').describe('error.length.invalid'), | ||
| }) | ||
| export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Strengthen server validation for email and birthDate
Use ArkType’s built-in predicates for email and ISO date instead of plain strings/length checks.
References: ArkType string predicates (e.g., string.email, string.date.iso). (arktype.io)
export const completeBonusProgramRegistrationSchema = type({
name: type('2 <= string <= 50').describe('error.length.invalid'),
surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(),
- email: type('2 <= string <= 80').describe('error.length.invalid'),
- birthDate: type('string').describe('error.length.invalid'),
+ email: type('2 <= string.email <= 80').describe('error.length.invalid'),
+ birthDate: type('string.date.iso').describe('error.length.invalid'),
})Optionally, add a “not in the future” guard for birthDate with a refinement if needed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const completeBonusProgramRegistrationSchema = type({ | |
| name: type('2 <= string <= 50').describe('error.length.invalid'), | |
| surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(), | |
| email: type('2 <= string <= 80').describe('error.length.invalid'), | |
| birthDate: type('string').describe('error.length.invalid'), | |
| }) | |
| export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer | |
| export const completeBonusProgramRegistrationSchema = type({ | |
| name: type('2 <= string <= 50').describe('error.length.invalid'), | |
| surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(), | |
| email: type('2 <= string.email <= 80').describe('error.length.invalid'), | |
| birthDate: type('string.date.iso').describe('error.length.invalid'), | |
| }) | |
| export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer |
🤖 Prompt for AI Agents
In apps/storefront-telegram/shared/services/client.ts around lines 8 to 14, the
schema currently uses generic string/length checks for email and birthDate;
replace those with ArkType string predicates: use string.email for the email
field and string.date.iso for birthDate (and update error messages if needed).
Ensure the surname remains optional with its length predicate. Optionally add a
refinement/guard on birthDate to reject ISO dates in the future (e.g., compare
parsed date to now) and return a clear validation message. Also confirm any
necessary imports from ArkType (string/date predicates) are present at the top
of the file.
| static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) { | ||
| const [client] = await useDatabase() | ||
| .update(clients) | ||
| .set({ | ||
| ...data, | ||
| bonusProgramParticipantFrom: sql`now()`, | ||
| updatedAt: sql`now()`, | ||
| }) | ||
| .where(eq(clients.id, id)) | ||
| .returning() | ||
| return client | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Preserve first participation date and avoid overwriting columns with undefined.
- Overwriting
bonusProgramParticipantFromwithnow()on every edit loses the original join date. - Spreading
...datamay pushundefinedinto nullable/non-nullable columns depending on Zod parsing; filter outundefinedkeys.
Use COALESCE and filter undefined:
static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) {
- const [client] = await useDatabase()
- .update(clients)
- .set({
- ...data,
- bonusProgramParticipantFrom: sql`now()`,
- updatedAt: sql`now()`,
- })
+ // Only persist provided fields; do not clobber with undefined.
+ const patch = Object.fromEntries(
+ Object.entries(data).filter(([, v]) => v !== undefined)
+ ) as Partial<ClientDraft>
+
+ const [client] = await useDatabase()
+ .update(clients)
+ .set({
+ ...patch,
+ // Keep the original participation timestamp if already set
+ bonusProgramParticipantFrom: sql`coalesce(${clients.bonusProgramParticipantFrom}, now())`,
+ updatedAt: sql`now()`,
+ })
.where(eq(clients.id, id))
.returning()
return client
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) { | |
| const [client] = await useDatabase() | |
| .update(clients) | |
| .set({ | |
| ...data, | |
| bonusProgramParticipantFrom: sql`now()`, | |
| updatedAt: sql`now()`, | |
| }) | |
| .where(eq(clients.id, id)) | |
| .returning() | |
| return client | |
| } | |
| static async updateBonusProgram( | |
| id: string, | |
| data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'> | |
| ) { | |
| // Only persist provided fields; do not clobber with undefined. | |
| const patch = Object.fromEntries( | |
| Object.entries(data).filter(([, v]) => v !== undefined) | |
| ) as Partial<ClientDraft> | |
| const [client] = await useDatabase() | |
| .update(clients) | |
| .set({ | |
| ...patch, | |
| // Keep the original participation timestamp if already set | |
| bonusProgramParticipantFrom: sql` | |
| coalesce(${clients.bonusProgramParticipantFrom}, now()) | |
| `, | |
| updatedAt: sql`now()`, | |
| }) | |
| .where(eq(clients.id, id)) | |
| .returning() | |
| return client | |
| } |
🤖 Prompt for AI Agents
In packages/database/src/repository/client.ts around lines 78 to 89, the update
currently overwrites bonusProgramParticipantFrom with now() on every edit and
spreads possibly-undefined values from data; change the update to (1) filter the
incoming data to remove keys with undefined so you only set provided fields, (2)
set bonusProgramParticipantFrom using a COALESCE-style expression so it keeps
the existing value if present and only sets now() when null/undefined, and (3)
always set updatedAt to now(); ensure the .set call only includes the filtered
fields plus the COALESCE expression and updatedAt, then return the updated
client as before.



Summary by CodeRabbit
New Features
Improvements
Localization
Chores