diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html
index d5b4ba6e..e0cd7821 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html
@@ -34,23 +34,25 @@
{{ reward.name }}
{{ 'REWARD_MANAGEMENT.METRIC_REDEMPTION' | translate }}
- {{ ctx.redemptionCount() }}
+ {{ ctx.kpiStats()?.redemptionCount ?? ctx.redemptionCount() }}
{{ 'REWARD_MANAGEMENT.METRIC_AVAILABILITY' | translate }}
- {{ ctx.availableCount() }} / {{ ctx.totalCount() }}
+ {{
+ ctx.kpiStats()?.availableStock ?? ctx.availableCount() + ' / ' + ctx.totalCount()
+ }}
{{ 'REWARD_MANAGEMENT.METRIC_COST' | translate }}
- ${{ reward.cost }}
+ ${{ ctx.kpiStats()?.totalCost ?? reward.cost }}
{{ 'REWARD_MANAGEMENT.METRIC_TOP' | translate }}
- {{ ctx.topRedeemed() }}
+ {{ ctx.kpiStats()?.topRedeemed ?? ctx.topRedeemed() }}
{{ 'REWARD_MANAGEMENT.METRIC_POINTS' | translate }}
- {{ reward.pointsValue }}
+ {{ ctx.kpiStats()?.pointsValue ?? reward.pointsValue }}
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html
index 47c03197..747540d6 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html
@@ -15,9 +15,6 @@
{{ 'REWARD_MANAGEMENT.IMAGE' | translate }}
- @if (isEditMode) {
- {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
- }
{{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
- @if (isEditMode) {
-
- @if (isInvalid('category')) {
- {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }}
- }
- } @else {
-
- @if (isInvalid('categoryId')) {
- {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }}
+
+ @if (isInvalid('categoryId')) {
+ {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }}
}
- @if (isEditMode) {
-
-
- {{ 'REWARD_MANAGEMENT.COST' | translate }}
- {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
-
-
-
- @if (isInvalid('cost')) {
- {{ 'REWARD_MANAGEMENT.COST_REQUIRED' | translate }}
- }
-
-
- }
-
{{ 'REWARD_MANAGEMENT.PRICE' | translate }}
@@ -186,26 +145,36 @@
{{ 'REWARD_MANAGEMENT.HOW_TO_REDEEM' | translate }}
+ {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
+ @if (isInvalid('howToRedeem')) {
+ {{ 'REWARD_MANAGEMENT.HOW_TO_REDEEM_REQUIRED' | translate }}
+ }
{{ 'REWARD_MANAGEMENT.TERMS' | translate }}
+ {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
+ @if (isInvalid('termsOfUse')) {
+ {{ 'REWARD_MANAGEMENT.TERMS_REQUIRED' | translate }}
+ }
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts
index dbd2b275..a3523d5e 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts
@@ -3,7 +3,7 @@ import { Component, OnDestroy, OnInit, inject, input, signal } from '@angular/co
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import type { CreateRewardDto, RewardCategory, RewardItem } from '../../models/reward.models';
+import type { RewardProfileDto } from '../../models/reward.models';
import { SEEDED_REWARD_CATEGORIES } from '../../models/reward.models';
import { RewardService } from '../../services/reward.service';
import { ModalComponent } from '@app/shared/components/modal/modal.component';
@@ -28,7 +28,6 @@ export class RewardFormComponent implements OnInit, OnDestroy {
readonly id = input
();
isEditMode = false;
- readonly categories = signal([]);
readonly seededCategories = SEEDED_REWARD_CATEGORIES;
readonly isSubmitting = signal(false);
readonly showSuccessModal = signal(false);
@@ -41,38 +40,19 @@ export class RewardFormComponent implements OnInit, OnDestroy {
imageUrl: [''],
name: ['', [Validators.required, Validators.minLength(2)]],
description: [''],
- category: [''],
- categoryId: [null as number | null],
- cost: [null as number | null],
+ categoryId: [null as number | null, [Validators.required]],
price: [null as number | null, [Validators.required, Validators.min(0)]],
pointsValue: [null as number | null, [Validators.required, Validators.min(1)]],
- howToRedeem: [''],
- termsOfUse: [''],
+ howToRedeem: ['', [Validators.required]],
+ termsOfUse: ['', [Validators.required]],
});
ngOnInit(): void {
const rewardId = this.id();
if (rewardId) {
this.isEditMode = true;
- this.form.controls.category.setValidators([Validators.required]);
- this.form.controls.cost.setValidators([Validators.required, Validators.min(0)]);
- this.form.controls.imageUrl.setValidators([Validators.required]);
- this.form.controls.pointsValue.setValidators([Validators.required, Validators.min(0)]);
this.loadReward(rewardId);
- this.rewardService.getCategories().subscribe({
- next: (categories) => this.categories.set(categories),
- });
- } else {
- this.form.controls.categoryId.setValidators([Validators.required]);
}
-
- this.form.controls.category.updateValueAndValidity();
- this.form.controls.categoryId.updateValueAndValidity();
- this.form.controls.cost.updateValueAndValidity();
- this.form.controls.imageUrl.updateValueAndValidity();
- this.form.controls.howToRedeem.updateValueAndValidity();
- this.form.controls.termsOfUse.updateValueAndValidity();
- this.form.controls.pointsValue.updateValueAndValidity();
}
ngOnDestroy(): void {
@@ -80,24 +60,28 @@ export class RewardFormComponent implements OnInit, OnDestroy {
}
loadReward(rewardId: string): void {
- this.rewardService.getReward(rewardId).subscribe({
- next: (reward) => this.patchForm(reward),
+ this.rewardService.getRewardProfile(rewardId).subscribe({
+ next: (response) => this.patchForm(response.profile),
error: () => this.loadError.set(true),
});
}
- patchForm(reward: RewardItem): void {
- this.imagePreview.set(reward.imageUrl);
+ patchForm(reward: RewardProfileDto): void {
+ const categoryId =
+ this.seededCategories.find(
+ (category) => category.name.toLowerCase() === reward.category.toLowerCase(),
+ )?.id ?? null;
+
+ this.imagePreview.set(reward.imageUrl ?? '');
this.form.patchValue({
- imageUrl: reward.imageUrl,
+ imageUrl: reward.imageUrl ?? '',
name: reward.name,
- description: reward.description,
- category: reward.category,
- cost: reward.cost,
- price: reward.price,
- pointsValue: reward.pointsValue,
- howToRedeem: reward.howToRedeem,
- termsOfUse: reward.termsOfUse,
+ description: reward.description ?? '',
+ categoryId,
+ price: reward.monetaryValue,
+ pointsValue: reward.points,
+ howToRedeem: reward.howToRedeem ?? '',
+ termsOfUse: reward.termsOfUse ?? '',
});
}
@@ -117,30 +101,19 @@ export class RewardFormComponent implements OnInit, OnDestroy {
return;
}
- if (!this.isEditMode) {
- if (!this.isAllowedImage(file)) {
- this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.IMAGE_INVALID'));
- inputEl.value = '';
- return;
- }
- this.submitError.set(null);
- this.revokePreview();
- this.imageFile.set(file);
- const preview = URL.createObjectURL(file);
- this.imagePreview.set(preview);
- this.form.patchValue({ imageUrl: preview });
- this.form.get('imageUrl')?.markAsTouched();
+ if (!this.isAllowedImage(file)) {
+ this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.IMAGE_INVALID'));
+ inputEl.value = '';
return;
}
- const reader = new FileReader();
- reader.onload = () => {
- const dataUrl = String(reader.result);
- this.imagePreview.set(dataUrl);
- this.form.patchValue({ imageUrl: dataUrl });
- this.form.get('imageUrl')?.markAsTouched();
- };
- reader.readAsDataURL(file);
+ this.submitError.set(null);
+ this.revokePreview();
+ this.imageFile.set(file);
+ const preview = URL.createObjectURL(file);
+ this.imagePreview.set(preview);
+ this.form.patchValue({ imageUrl: preview });
+ this.form.get('imageUrl')?.markAsTouched();
}
removeImage(): void {
@@ -168,95 +141,49 @@ export class RewardFormComponent implements OnInit, OnDestroy {
this.isSubmitting.set(true);
this.submitError.set(null);
- if (this.isEditMode) {
- this.submitEdit();
- return;
- }
-
- this.submitCreate();
- }
-
- confirmSuccess(): void {
- this.showSuccessModal.set(false);
- this.router.navigate(['/rewards']);
- }
-
- private submitCreate(): void {
- const value = this.form.getRawValue();
- this.rewardService
- .createReward({
- name: value.name!.trim(),
- description: value.description?.trim() ?? '',
- categoryId: Number(value.categoryId),
- points: Number(value.pointsValue),
- monetaryValue: Number(value.price),
- howToRedeem: value.howToRedeem?.trim() ?? '',
- termsOfUse: value.termsOfUse?.trim() ?? '',
- imageFile: this.imageFile(),
- })
- .subscribe({
- next: () => {
- this.isSubmitting.set(false);
- this.showSuccessModal.set(true);
- },
- error: (err: unknown) => {
- this.isSubmitting.set(false);
- this.submitError.set(this.extractErrorMessage(err));
- },
- });
- }
-
- private submitEdit(): void {
const value = this.form.getRawValue();
- const dto: CreateRewardDto = {
+ const payload = {
name: value.name!.trim(),
description: value.description?.trim() ?? '',
- category: value.category!,
- imageUrl: value.imageUrl!,
- cost: Number(value.cost),
- price: Number(value.price),
- pointsValue: Number(value.pointsValue),
+ categoryId: Number(value.categoryId),
+ points: Number(value.pointsValue),
+ monetaryValue: Number(value.price),
howToRedeem: value.howToRedeem?.trim() ?? '',
termsOfUse: value.termsOfUse?.trim() ?? '',
- status: 'Active',
- availableStock: 0,
- createdAt: new Date().toISOString(),
+ imageFile: this.imageFile(),
};
const rewardId = this.id();
- if (!rewardId) {
- this.isSubmitting.set(false);
- return;
- }
+ const request$ =
+ this.isEditMode && rewardId
+ ? this.rewardService.updateReward(rewardId, payload)
+ : this.rewardService.createReward(payload);
+
+ request$.subscribe({
+ next: () => {
+ this.isSubmitting.set(false);
+ this.showSuccessModal.set(true);
+ },
+ error: (err: unknown) => {
+ this.isSubmitting.set(false);
+ this.submitError.set(
+ this.extractErrorMessage(
+ err,
+ this.isEditMode ? 'REWARD_MANAGEMENT.UPDATE_ERROR' : 'REWARD_MANAGEMENT.CREATE_ERROR',
+ ),
+ );
+ },
+ });
+ }
- this.rewardService
- .updateReward(rewardId, {
- name: dto.name,
- description: dto.description,
- category: dto.category,
- imageUrl: dto.imageUrl,
- cost: dto.cost,
- price: dto.price,
- pointsValue: dto.pointsValue,
- howToRedeem: dto.howToRedeem,
- termsOfUse: dto.termsOfUse,
- })
- .subscribe({
- next: () => {
- this.isSubmitting.set(false);
- this.showSuccessModal.set(true);
- },
- error: () => {
- this.isSubmitting.set(false);
- this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.UPDATE_ERROR'));
- },
- });
+ confirmSuccess(): void {
+ this.showSuccessModal.set(false);
+ this.router.navigate(['/rewards']);
}
private isAllowedImage(file: File): boolean {
const typeOk =
- ALLOWED_IMAGE_TYPES.includes(file.type) ||
- /\.(jpe?g|png)$/i.test(file.name);
+ ALLOWED_IMAGE_TYPES.includes(file.type) || /\.(jpe?g|png)$/i.test(file.name);
return typeOk && file.size > 0 && file.size <= MAX_IMAGE_BYTES;
}
@@ -267,7 +194,7 @@ export class RewardFormComponent implements OnInit, OnDestroy {
}
}
- private extractErrorMessage(err: unknown): string {
+ private extractErrorMessage(err: unknown, fallbackKey: string): string {
if (err instanceof HttpErrorResponse) {
if (typeof err.error?.message === 'string' && err.error.message.trim()) {
return err.error.message;
@@ -276,6 +203,6 @@ export class RewardFormComponent implements OnInit, OnDestroy {
return err.error;
}
}
- return this.translate.instant('REWARD_MANAGEMENT.CREATE_ERROR');
+ return this.translate.instant(fallbackKey);
}
}
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts
index 288c879a..473e7de8 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts
@@ -21,6 +21,7 @@ import {
} from '@app/shared/components/table/table.component';
import { ModalComponent } from '@app/shared/components/modal/modal.component';
import { ModalBodyComponent } from '@app/shared/components/modal/modal-body.component';
+import { HttpErrorResponse } from '@angular/common/http';
import type {
RewardListDto,
RewardListFilter,
@@ -272,9 +273,9 @@ export class RewardListComponent implements AfterViewInit, OnDestroy {
this.deletingReward.set(null);
this.showSuccessModal.set(true);
},
- error: () => {
+ error: (err: unknown) => {
this.isDeleting.set(false);
- this.deleteError.set(this.translate.instant('REWARD_MANAGEMENT.DELETE_ERROR'));
+ this.deleteError.set(this.extractDeleteError(err));
},
});
}
@@ -294,6 +295,18 @@ export class RewardListComponent implements AfterViewInit, OnDestroy {
);
}
+ private extractDeleteError(err: unknown): string {
+ if (err instanceof HttpErrorResponse) {
+ if (typeof err.error?.message === 'string' && err.error.message.trim()) {
+ return err.error.message;
+ }
+ if (typeof err.error === 'string' && err.error.trim()) {
+ return err.error;
+ }
+ }
+ return this.translate.instant('REWARD_MANAGEMENT.DELETE_ERROR');
+ }
+
private currentFilter(): RewardListFilter {
const date = this.customDate();
return {
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts
index 7677b2f0..251382af 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts
@@ -63,7 +63,20 @@ export interface RewardProfileDto {
readonly isActive: boolean;
}
-export interface CreateRewardApiInput {
+export interface RewardKpiStatistics {
+ readonly redemptionCount: number;
+ readonly availableStock: string;
+ readonly totalCost: number;
+ readonly topRedeemed: number;
+ readonly pointsValue: number;
+}
+
+export interface RewardProfileResponseDto {
+ readonly profile: RewardProfileDto;
+ readonly kpiStats: RewardKpiStatistics;
+}
+
+export interface RewardWriteInput {
readonly name: string;
readonly description: string;
readonly categoryId: number;
@@ -74,6 +87,8 @@ export interface CreateRewardApiInput {
readonly imageFile?: File | null;
}
+export type CreateRewardApiInput = RewardWriteInput;
+
export interface RewardItem {
id: string;
name: string;
@@ -90,6 +105,35 @@ export interface RewardItem {
createdAt: string;
}
+export function mapRewardProfileToItem(
+ profile: RewardProfileDto,
+ kpi?: RewardKpiStatistics | null,
+): RewardItem {
+ return {
+ id: String(profile.id),
+ name: profile.name,
+ description: profile.description ?? '',
+ category: profile.category,
+ imageUrl: profile.imageUrl ?? '',
+ cost: kpi?.totalCost ?? 0,
+ price: profile.monetaryValue,
+ pointsValue: profile.points,
+ howToRedeem: profile.howToRedeem,
+ termsOfUse: profile.termsOfUse,
+ status: profile.isActive ? 'Active' : 'Inactive',
+ availableStock: firstStockCount(kpi?.availableStock),
+ createdAt: '',
+ };
+}
+
+function firstStockCount(availableStock?: string): number {
+ if (!availableStock) {
+ return 0;
+ }
+ const value = Number(availableStock.split('/')[0]);
+ return Number.isFinite(value) ? value : 0;
+}
+
export type CreateRewardDto = Omit;
export interface RewardRedemption {
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts
index 296d9f71..f0c558dc 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts
@@ -1,11 +1,13 @@
import { Injectable, computed, inject, signal } from '@angular/core';
-import { forkJoin } from 'rxjs';
+import { catchError, forkJoin, of } from 'rxjs';
import type {
EmployeeName,
RewardInventoryItem,
RewardItem,
+ RewardKpiStatistics,
RewardRedemption,
} from '../models/reward.models';
+import { mapRewardProfileToItem } from '../models/reward.models';
import { RewardService } from './reward.service';
@Injectable({ providedIn: 'root' })
@@ -14,6 +16,7 @@ export class RewardDetailsContext {
readonly rewardId = signal('');
readonly reward = signal(null);
+ readonly kpiStats = signal(null);
readonly inventory = signal([]);
readonly redemptions = signal([]);
readonly employees = signal([]);
@@ -49,13 +52,14 @@ export class RewardDetailsContext {
this.loadError.set(false);
forkJoin({
- reward: this.rewardService.getReward(rewardId),
- inventory: this.rewardService.getInventory(rewardId),
- redemptions: this.rewardService.getRedemptions(),
- employees: this.rewardService.getEmployees(),
+ details: this.rewardService.getRewardProfile(rewardId),
+ inventory: this.rewardService.getInventory(rewardId).pipe(catchError(() => of([]))),
+ redemptions: this.rewardService.getRedemptions().pipe(catchError(() => of([]))),
+ employees: this.rewardService.getEmployees().pipe(catchError(() => of([]))),
}).subscribe({
- next: ({ reward, inventory, redemptions, employees }) => {
- this.reward.set(reward);
+ next: ({ details, inventory, redemptions, employees }) => {
+ this.reward.set(mapRewardProfileToItem(details.profile, details.kpiStats));
+ this.kpiStats.set(details.kpiStats);
this.inventory.set(inventory);
this.redemptions.set(
redemptions.filter((item) => String(item.rewardItemId) === String(rewardId)),
@@ -87,13 +91,8 @@ export class RewardDetailsContext {
}
this.toggling.set(true);
const status = reward.status === 'Active' ? 'Inactive' : 'Active';
- this.rewardService.updateReward(reward.id, { status }).subscribe({
- next: (updated) => {
- this.reward.set({ ...reward, ...updated, status });
- this.toggling.set(false);
- },
- error: () => this.toggling.set(false),
- });
+ this.reward.set({ ...reward, status });
+ this.toggling.set(false);
}
private redemptionDates(): string[] {
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts
index 1c3a4c69..ed11a268 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts
@@ -6,7 +6,6 @@ import { environment } from '../../../../environments/environment';
import { RewardService } from './reward.service';
const REWARDS_URL = `${environment.baseUrl}/rewards`;
-const MOCK_ITEMS_URL = `${environment.jsonServerUrl}/rewardItems`;
describe('RewardService', () => {
let service: RewardService;
@@ -131,25 +130,112 @@ describe('RewardService', () => {
);
});
- it('should keep get-by-id on json-server', () => {
- service.getReward('8').subscribe();
+ it('should PUT update as multipart FormData to /rewards/{id}', () => {
+ const file = new File(['img'], 'banner.png', { type: 'image/png' });
- const req = httpMock.expectOne(`${MOCK_ITEMS_URL}/8`);
- expect(req.request.method).toBe('GET');
+ service
+ .updateReward(12, {
+ name: 'Amazon Card',
+ description: 'Gift',
+ categoryId: 1,
+ points: 200,
+ monetaryValue: 50,
+ howToRedeem: 'Show code',
+ termsOfUse: 'No cash',
+ imageFile: file,
+ })
+ .subscribe((result) => {
+ expect(result.id).toBe(12);
+ });
+
+ const req = httpMock.expectOne(`${REWARDS_URL}/12`);
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body instanceof FormData).toBe(true);
+ const body = req.request.body as FormData;
+ expect(body.get('name')).toBe('Amazon Card');
+ expect(body.get('categoryId')).toBe('1');
+ expect(body.get('imageFile')).toBeTruthy();
req.flush({
- id: '8',
- name: 'Mock',
- description: '',
+ id: 12,
+ name: 'Amazon Card',
+ description: 'Gift',
+ imageUrl: null,
category: 'Gift Cards',
- imageUrl: '',
- cost: 1,
- price: 1,
- pointsValue: 1,
- howToRedeem: '',
- termsOfUse: '',
- status: 'Active',
- availableStock: 0,
- createdAt: '2026-01-01T00:00:00Z',
+ points: 200,
+ monetaryValue: 50,
+ howToRedeem: 'Show code',
+ termsOfUse: 'No cash',
+ isActive: true,
+ });
+ });
+
+ it('should DELETE a reward by id on the real API', () => {
+ service.deleteReward(12).subscribe();
+
+ const req = httpMock.expectOne(`${REWARDS_URL}/12`);
+ expect(req.request.method).toBe('DELETE');
+ req.flush(null, { status: 204, statusText: 'No Content' });
+ });
+
+ it('should GET reward profile by id from the real API', () => {
+ service.getRewardProfile(12).subscribe((response) => {
+ expect(response.profile.name).toBe('Amazon Card');
+ });
+
+ const req = httpMock.expectOne(`${REWARDS_URL}/12`);
+ expect(req.request.method).toBe('GET');
+ req.flush({
+ profile: {
+ id: 12,
+ name: 'Amazon Card',
+ description: 'Gift',
+ imageUrl: null,
+ category: 'Gift Cards',
+ points: 200,
+ monetaryValue: 50,
+ howToRedeem: 'Show code',
+ termsOfUse: 'No cash',
+ isActive: true,
+ },
+ kpiStats: {
+ redemptionCount: 0,
+ availableStock: '0/0',
+ totalCost: 0,
+ topRedeemed: 0,
+ pointsValue: 200,
+ },
+ });
+ });
+
+ it('should map GET reward by id from the real API profile', () => {
+ service.getReward('8').subscribe((reward) => {
+ expect(reward.id).toBe('8');
+ expect(reward.name).toBe('Mock');
+ expect(reward.status).toBe('Active');
+ });
+
+ const req = httpMock.expectOne(`${REWARDS_URL}/8`);
+ expect(req.request.method).toBe('GET');
+ req.flush({
+ profile: {
+ id: 8,
+ name: 'Mock',
+ description: '',
+ imageUrl: null,
+ category: 'Gift Cards',
+ points: 1,
+ monetaryValue: 1,
+ howToRedeem: '',
+ termsOfUse: '',
+ isActive: true,
+ },
+ kpiStats: {
+ redemptionCount: 0,
+ availableStock: '0/0',
+ totalCost: 0,
+ topRedeemed: 0,
+ pointsValue: 1,
+ },
});
});
});
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts
index aa8d6226..103c7cb8 100644
--- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts
+++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts
@@ -3,18 +3,19 @@ import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
-import type {
- CreateInventoryDto,
- CreateRewardApiInput,
- CreateRewardDto,
- EmployeeName,
- PaginatedRewards,
- RewardCategory,
- RewardInventoryItem,
- RewardItem,
- RewardListFilter,
- RewardProfileDto,
- RewardRedemption,
+import {
+ mapRewardProfileToItem,
+ type CreateInventoryDto,
+ type EmployeeName,
+ type PaginatedRewards,
+ type RewardCategory,
+ type RewardInventoryItem,
+ type RewardItem,
+ type RewardListFilter,
+ type RewardProfileDto,
+ type RewardProfileResponseDto,
+ type RewardRedemption,
+ type RewardWriteInput,
} from '../models/reward.models';
const REWARDS_API = `${environment.baseUrl}/rewards`;
@@ -46,7 +47,7 @@ export function buildRewardListParams(filter: RewardListFilter): HttpParams {
return params;
}
-export function buildCreateRewardFormData(input: CreateRewardApiInput): FormData {
+export function buildRewardFormData(input: RewardWriteInput): FormData {
const formData = new FormData();
formData.append('name', input.name);
formData.append('description', input.description);
@@ -66,7 +67,6 @@ export function buildCreateRewardFormData(input: CreateRewardApiInput): FormData
})
export class RewardService {
private readonly http = inject(HttpClient);
- private readonly mockApiUrl = `${environment.jsonServerUrl}/rewardItems`;
private readonly categoriesUrl = `${environment.jsonServerUrl}/rewardCategories`;
private readonly redemptionsUrl = `${environment.jsonServerUrl}/rewardRedemptions`;
private readonly inventoryUrl = `${environment.jsonServerUrl}/rewardInventory`;
@@ -78,20 +78,26 @@ export class RewardService {
});
}
+ getRewardProfile(id: number | string): Observable {
+ return this.http.get(`${REWARDS_API}/${id}`);
+ }
+
getReward(id: string): Observable {
- return this.http.get(`${this.mockApiUrl}/${id}`);
+ return this.getRewardProfile(id).pipe(
+ map((response) => mapRewardProfileToItem(response.profile, response.kpiStats)),
+ );
}
- createReward(input: CreateRewardApiInput): Observable {
- return this.http.post(REWARDS_API, buildCreateRewardFormData(input));
+ createReward(input: RewardWriteInput): Observable {
+ return this.http.post(REWARDS_API, buildRewardFormData(input));
}
- updateReward(id: string, dto: Partial): Observable {
- return this.http.patch(`${this.mockApiUrl}/${id}`, dto);
+ updateReward(id: number | string, input: RewardWriteInput): Observable {
+ return this.http.put(`${REWARDS_API}/${id}`, buildRewardFormData(input));
}
deleteReward(id: string | number): Observable {
- return this.http.delete(`${this.mockApiUrl}/${id}`);
+ return this.http.delete(`${REWARDS_API}/${id}`);
}
getCategories(): Observable {