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:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -321,6 +321,7 @@ export const hy: Translations = {
|
||||
compare: 'Համեմատել',
|
||||
share: 'Կիսվել',
|
||||
notifyMe: 'Ծանուցել ինձ',
|
||||
notifyMeConfirmed: 'Մենք կտեղեկացնենք ձեզ, երբ ապրանքը կրկին հասանելի լինի։',
|
||||
zoom: 'Մեծացնել',
|
||||
fullscreen: 'Ամբողջ էկրան',
|
||||
pdfDocument: 'Ապրանքի PDF փաստաթուղթ',
|
||||
|
||||
@@ -321,6 +321,7 @@ export const ru: Translations = {
|
||||
compare: 'Сравнить',
|
||||
share: 'Поделиться',
|
||||
notifyMe: 'Сообщить о наличии',
|
||||
notifyMeConfirmed: 'Мы сообщим вам, когда товар снова появится в наличии.',
|
||||
zoom: 'Увеличить',
|
||||
fullscreen: 'Полный экран',
|
||||
pdfDocument: 'PDF документ товара',
|
||||
|
||||
@@ -319,6 +319,7 @@ export interface Translations {
|
||||
compare: string;
|
||||
share: string;
|
||||
notifyMe: string;
|
||||
notifyMeConfirmed: string;
|
||||
zoom: string;
|
||||
fullscreen: string;
|
||||
pdfDocument: string;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user