feat: real back-in-stock subscription for Notify Me (API + localStorage fallback)

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 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 09:30:53 +04:00
parent 1af337f005
commit 0b08802996
7 changed files with 64 additions and 1 deletions

View File

@@ -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<Required<ProductPageConfig>>(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<number[]>(RESTOCK_SUBSCRIPTIONS_KEY) ?? [];
if (!existing.includes(itemID)) {
this.storage.setJSON(RESTOCK_SUBSCRIPTIONS_KEY, [...existing, itemID]);
}
}
addRelatedToCart(payload: { product: Product; event: Event }): void {

View File

@@ -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',

View File

@@ -321,6 +321,7 @@ export const hy: Translations = {
compare: 'Համեմատել',
share: 'Կիսվել',
notifyMe: 'Ծանուցել ինձ',
notifyMeConfirmed: 'Մենք կտեղեկացնենք ձեզ, երբ ապրանքը կրկին հասանելի լինի։',
zoom: 'Մեծացնել',
fullscreen: 'Ամբողջ էկրան',
pdfDocument: 'Ապրանքի PDF փաստաթուղթ',

View File

@@ -321,6 +321,7 @@ export const ru: Translations = {
compare: 'Сравнить',
share: 'Поделиться',
notifyMe: 'Сообщить о наличии',
notifyMeConfirmed: 'Мы сообщим вам, когда товар снова появится в наличии.',
zoom: 'Увеличить',
fullscreen: 'Полный экран',
pdfDocument: 'PDF документ товара',

View File

@@ -319,6 +319,7 @@ export interface Translations {
compare: string;
share: string;
notifyMe: string;
notifyMeConfirmed: string;
zoom: string;
fullscreen: string;
pdfDocument: string;

View File

@@ -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<void> {
return this.http.post<void>(`${this.baseUrl}/items/${itemID}/notify-me`, contact);
}
getRandomItems(count: number = 5, categoryID?: number): Observable<Item[]> {
let params = new HttpParams().set('count', count.toString());
if (categoryID) {