From 0b0880299640d67911199466984604f5489ecebd Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 13 Aug 2026 09:30:53 +0400 Subject: [PATCH] feat: real back-in-stock subscription for Notify Me (API + localStorage fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit notifyMe() just called toggleWishlist() - no actual subscription mechanism existed. Now calls a new subscribeToRestock() API method (POST /items/{id}/notify-me, not yet built server-side - see BACKEND-API-REFERENCE.md §12.5) and falls back to a local-only record in localStorage on failure, so the request isn't silently dropped while the backend catches up. Co-Authored-By: Claude Sonnet 5 --- BACKEND-API-REFERENCE.md | 10 +++++ .../product-details-container.component.ts | 40 ++++++++++++++++++- src/app/i18n/en.ts | 1 + src/app/i18n/hy.ts | 1 + src/app/i18n/ru.ts | 1 + src/app/i18n/translations.ts | 1 + src/app/services/api.service.ts | 11 +++++ 7 files changed, 64 insertions(+), 1 deletion(-) diff --git a/BACKEND-API-REFERENCE.md b/BACKEND-API-REFERENCE.md index d2df1b5..79459a5 100644 --- a/BACKEND-API-REFERENCE.md +++ b/BACKEND-API-REFERENCE.md @@ -512,3 +512,13 @@ Separately, `createCartPayment()` (payment-gateway charge creation) still sends } ``` `actor` must be derived server-side from the authenticated caller, never trusted from the request body. + +### 12.5 Back-in-stock ("Notify Me") subscription + +**Gap:** the "Notify Me" button on out-of-stock products had no real subscription mechanism at all - it just toggled wishlist. Client fix already shipped: `notifyMe()` now calls `POST /items/{id}/notify-me` and, if that fails (today it always will - the endpoint doesn't exist), falls back to a local-only record in `localStorage['restockSubscriptions']` so the request isn't silently dropped while waiting on the backend. The shopper sees the same confirmation either way. + +**Ask:** implement `POST /items/{id}/notify-me`, plus whatever mechanism actually sends the notification once the item restocks (Telegram message, most likely, given the rest of the auth stack). Request body sent today: +```json +{ "telegramUserId": "8823771" } +``` +`telegramUserId` may be `null` for a non-Telegram web session - decide whether to also accept an email address as an alternative identifier (the frontend has no email capture on this flow today, so that would need a small frontend addition too). Once this ships, the frontend's localStorage fallback becomes purely a resilience path rather than the common case, and could optionally sync any locally-queued subscriptions on next successful call. diff --git a/src/app/features/website/product/containers/product-details-container.component.ts b/src/app/features/website/product/containers/product-details-container.component.ts index d850e1d..539bc9a 100644 --- a/src/app/features/website/product/containers/product-details-container.component.ts +++ b/src/app/features/website/product/containers/product-details-container.component.ts @@ -17,6 +17,12 @@ import { DEFAULT_PRODUCT_PAGE_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG, ProductPag import { getStockStatus, getTranslatedField } from '../../../../utils/item.utils'; import { ProductShareService } from '../../user-experience/services/product-share.service'; import { SeoService } from '../../../../services/seo.service'; +import { ApiService } from '../../../../services/api.service'; +import { LocalStorageService } from '../../../../core/storage/local-storage.service'; +import { UserNotificationService } from '../../user-experience/services/user-notification.service'; +import { AuthService } from '../../../../services/auth.service'; + +const RESTOCK_SUBSCRIPTIONS_KEY = 'restockSubscriptions'; import { ProductDeliveryInformationComponent } from '../components/delivery-information/delivery-information.component'; import { ProductActionsComponent } from '../components/product-actions/product-actions.component'; import { ProductGalleryComponent } from '../components/product-gallery/product-gallery.component'; @@ -72,6 +78,10 @@ export class ProductDetailsContainerComponent { private readonly translate = inject(TranslateService); private readonly shareService = inject(ProductShareService); private readonly seoService = inject(SeoService); + private readonly apiService = inject(ApiService); + private readonly storage = inject(LocalStorageService); + private readonly notifications = inject(UserNotificationService); + private readonly authService = inject(AuthService); readonly productPageConfigState = signal>(this.resolveProductPageConfig()); readonly userExperienceConfig = signal(this.resolveUserExperienceConfig()); @@ -358,8 +368,36 @@ export class ProductDetailsContainerComponent { await this.shareService.shareProduct(current, url); } + /** + * Back-in-stock subscription. Tries the backend endpoint first (not built + * yet - see BACKEND-API-REFERENCE.md §12); falls back to a local-only + * record on any failure so the request isn't silently lost while the + * backend catches up. Either way the shopper sees the same confirmation. + */ notifyMe(): void { - this.toggleWishlist(); + const current = this.product(); + if (!current) { + return; + } + + const telegramUserId = this.authService.session()?.userId != null + ? String(this.authService.session()!.userId) + : null; + + this.apiService.subscribeToRestock(current.itemID, { telegramUserId }).subscribe({ + next: () => this.notifications.show(this.translate.t('productDetails.notifyMeConfirmed'), 'success'), + error: () => { + this.saveRestockSubscriptionLocally(current.itemID); + this.notifications.show(this.translate.t('productDetails.notifyMeConfirmed'), 'success'); + } + }); + } + + private saveRestockSubscriptionLocally(itemID: number): void { + const existing = this.storage.getJSON(RESTOCK_SUBSCRIPTIONS_KEY) ?? []; + if (!existing.includes(itemID)) { + this.storage.setJSON(RESTOCK_SUBSCRIPTIONS_KEY, [...existing, itemID]); + } } addRelatedToCart(payload: { product: Product; event: Event }): void { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index ddb80ce..ebfdf25 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -321,6 +321,7 @@ export const en: Translations = { compare: 'Compare', share: 'Share', notifyMe: 'Notify me', + notifyMeConfirmed: 'We\'ll let you know when this is back in stock.', zoom: 'Zoom', fullscreen: 'Fullscreen', pdfDocument: 'Product PDF document', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 991d790..0aed06c 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -321,6 +321,7 @@ export const hy: Translations = { compare: 'Համեմատել', share: 'Կիսվել', notifyMe: 'Ծանուցել ինձ', + notifyMeConfirmed: 'Մենք կտեղեկացնենք ձեզ, երբ ապրանքը կրկին հասանելի լինի։', zoom: 'Մեծացնել', fullscreen: 'Ամբողջ էկրան', pdfDocument: 'Ապրանքի PDF փաստաթուղթ', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 3988a0e..61a1d11 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -321,6 +321,7 @@ export const ru: Translations = { compare: 'Сравнить', share: 'Поделиться', notifyMe: 'Сообщить о наличии', + notifyMeConfirmed: 'Мы сообщим вам, когда товар снова появится в наличии.', zoom: 'Увеличить', fullscreen: 'Полный экран', pdfDocument: 'PDF документ товара', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index b8ef180..ca90cfa 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -319,6 +319,7 @@ export interface Translations { compare: string; share: string; notifyMe: string; + notifyMeConfirmed: string; zoom: string; fullscreen: string; pdfDocument: string; diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index d75efed..48da298 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -694,6 +694,17 @@ export class ApiService { return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData); } + /** + * Back-in-stock subscription. No backend endpoint exists for this yet + * (tracked in BACKEND-API-REFERENCE.md §12) - callers should fall back to + * a local-only record (see CartService/UX facade patterns) when this 404s + * or the request otherwise fails, rather than surfacing an error to the + * shopper for something this minor. + */ + subscribeToRestock(itemID: number, contact: { telegramUserId: string | null; email?: string }): Observable { + return this.http.post(`${this.baseUrl}/items/${itemID}/notify-me`, contact); + } + getRandomItems(count: number = 5, categoryID?: number): Observable { let params = new HttpParams().set('count', count.toString()); if (categoryID) {