Überarbeitung des Stripe Prozesses

This commit is contained in:
Andreas Knuth 2024-08-23 19:54:55 +02:00
parent 7a286e3519
commit 74d5f92aba
18 changed files with 164 additions and 68 deletions

View File

@ -31,9 +31,8 @@ export const users = pgTable('users', {
updated: timestamp('updated'), updated: timestamp('updated'),
latitude: doublePrecision('latitude'), latitude: doublePrecision('latitude'),
longitude: doublePrecision('longitude'), longitude: doublePrecision('longitude'),
stripeCustomerId: text('stripeCustomerId'),
subscriptionId: text('subscriptionId'), subscriptionId: text('subscriptionId'),
subscriptionPlan: subscriptionTypeEnum('subscriptionType'), subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'),
// embedding: vector('embedding', { dimensions: 1536 }), // embedding: vector('embedding', { dimensions: 1536 }),
}); });

View File

@ -60,7 +60,7 @@
<p>Your subscription details are as follows:</p> <p>Your subscription details are as follows:</p>
<p><span class="plan-info">{{#if (eq subscriptionPlan "professional")}}Professional (CPA, Attorney, Title Company) Plan{{else if (eq subscriptionPlan "broker")}}Business Broker Plan{{/if}}</span></p> <p><span class="plan-info">{{#if (eq subscriptionPlan "professional")}}Professional Plan (CPA, Attorney, Title Company, Surveyor, Appraiser){{else if (eq subscriptionPlan "broker")}}Business Broker Plan{{/if}}</span></p>
<p>If you have any questions or need further assistance, please feel free to contact our support team at any time.</p> <p>If you have any questions or need further assistance, please feel free to contact our support team at any time.</p>

View File

@ -12,7 +12,7 @@ async function bootstrap() {
origin: '*', origin: '*',
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App //origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: 'Content-Type, Accept, Authorization', allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
}); });
//origin: 'http://localhost:4200', //origin: 'http://localhost:4200',
await app.listen(3000); await app.listen(3000);

View File

@ -157,10 +157,7 @@ export const UserSchema = z
customerSubType: CustomerSubTypeEnum.optional().nullable(), customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(), created: z.date().optional().nullable(),
updated: z.date().optional().nullable(), updated: z.date().optional().nullable(),
stripeCustomerId: z.string().optional().nullable(),
subscriptionId: z.string().optional().nullable(), subscriptionId: z.string().optional().nullable(),
planActive: z.boolean().optional().nullable(),
planExpires: z.date().optional().nullable(),
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(), subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {

View File

@ -322,10 +322,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
customerSubType: null, customerSubType: null,
created: new Date(), created: new Date(),
updated: new Date(), updated: new Date(),
stripeCustomerId: null,
subscriptionId: null, subscriptionId: null,
planActive: false,
planExpires: null,
subscriptionPlan: subscriptionPlan, subscriptionPlan: subscriptionPlan,
}; };
} }

View File

@ -21,7 +21,10 @@ export class PaymentController {
const signature = req.headers['stripe-signature'] as string; const signature = req.headers['stripe-signature'] as string;
try { try {
const event = await this.paymentService.constructEvent(req.body, signature); // Konvertieren Sie den req.body Buffer in einen lesbaren String
const payload = req.body instanceof Buffer ? req.body.toString('utf8') : req.body;
const event = await this.paymentService.constructEvent(payload, signature);
// const event = await this.paymentService.constructEvent(req.body, signature);
if (event.type === 'checkout.session.completed') { if (event.type === 'checkout.session.completed') {
await this.paymentService.handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session); await this.paymentService.handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);

View File

@ -43,6 +43,14 @@ export class PaymentService {
const newCustomer = await this.stripe.customers.create({ const newCustomer = await this.stripe.customers.create({
email: checkout.email, email: checkout.email,
name: checkout.name, name: checkout.name,
shipping: {
name: checkout.name,
address: {
city: '',
state: '',
country: 'US',
},
},
}); });
customerId = newCustomer.id; customerId = newCustomer.id;
} }
@ -60,7 +68,6 @@ export class PaymentService {
], ],
success_url: `${process.env.WEB_HOST}/success`, success_url: `${process.env.WEB_HOST}/success`,
cancel_url: `${process.env.WEB_HOST}/pricing`, cancel_url: `${process.env.WEB_HOST}/pricing`,
// customer_email: checkout.email,
customer: customerId, customer: customerId,
shipping_address_collection: { shipping_address_collection: {
allowed_countries: ['US'], allowed_countries: ['US'],
@ -84,7 +91,7 @@ export class PaymentService {
return this.stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!); return this.stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} }
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> { async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
this.logger.info(JSON.stringify(session)); try {
const keycloakUsers = await this.authService.getUsers(); const keycloakUsers = await this.authService.getUsers();
const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email); const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email);
const user = await this.userService.getUserByMail(session.customer_details.email, { const user = await this.userService.getUserByMail(session.customer_details.email, {
@ -94,15 +101,19 @@ export class PaymentService {
username: keycloakUser.email, username: keycloakUser.email,
roles: [], roles: [],
}); });
user.stripeCustomerId = session.customer as string; this.logger.info(JSON.stringify(session));
user.subscriptionId = session.subscription as string; user.subscriptionId = session.subscription as string;
const subscription = await this.stripe.subscriptions.retrieve(user.subscriptionId);
user.customerType = 'professional'; user.customerType = 'professional';
if (session.metadata['plan'] === 'Broker Plan') { if (subscription.metadata['plan'] === 'Broker Plan') {
user.customerSubType = 'broker'; user.customerSubType = 'broker';
} }
user.subscriptionPlan = session.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker'; user.subscriptionPlan = subscription.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
this.userService.saveUser(user); await this.userService.saveUser(user, false);
this.mailService.sendSubscriptionConfirmation(user); await this.mailService.sendSubscriptionConfirmation(user);
} catch (error) {
this.logger.error(error);
}
} }
async getSubscription(email: string): Promise<Stripe.Subscription[]> { async getSubscription(email: string): Promise<Stripe.Subscription[]> {
const existingCustomers = await this.stripe.customers.list({ const existingCustomers = await this.stripe.customers.list({

View File

@ -1,6 +1,7 @@
import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common'; import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ZodError } from 'zod';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { User } from '../models/db.model'; import { User } from '../models/db.model';
@ -31,13 +32,32 @@ export class UserController {
return user; return user;
} }
@Post() @Post()
save(@Body() user: any): Promise<User> { async save(@Body() user: any): Promise<User> {
this.logger.info(`Saving user: ${JSON.stringify(user)}`); this.logger.info(`Saving user: ${JSON.stringify(user)}`);
const savedUser = this.userService.saveUser(user); try {
const savedUser = await this.userService.saveUser(user);
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`); this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
return savedUser; return savedUser;
} catch (error) {
if (error instanceof ZodError) {
const filteredErrors = error.errors
.map(item => ({
...item,
field: item.path[0],
}))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
}
throw error; // Andere Fehler einfach durchreichen
}
}
@Post('guaranteed')
async saveGuaranteed(@Body() user: any): Promise<User> {
this.logger.info(`Saving user guaranteed: ${JSON.stringify(user)}`);
const savedUser = await this.userService.saveUser(user, false);
this.logger.info(`User persisted guaranteed: ${JSON.stringify(savedUser)}`);
return savedUser;
} }
@Post('search') @Post('search')
find(@Body() criteria: UserListingCriteria): any { find(@Body() criteria: UserListingCriteria): any {
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`); this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);

View File

@ -1,9 +1,8 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm'; import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ZodError } from 'zod';
import * as schema from '../drizzle/schema.js'; import * as schema from '../drizzle/schema.js';
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js'; import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
@ -97,7 +96,7 @@ export class UserService {
.where(sql`email = ${email}`)) as User[]; .where(sql`email = ${email}`)) as User[];
if (users.length === 0) { if (users.length === 0) {
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) }; const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) };
const u = await this.saveUser(user); const u = await this.saveUser(user, false);
return convertDrizzleUserToUser(u); return convertDrizzleUserToUser(u);
} else { } else {
const user = users[0]; const user = users[0];
@ -117,7 +116,7 @@ export class UserService {
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return convertDrizzleUserToUser(user); return convertDrizzleUserToUser(user);
} }
async saveUser(user: User): Promise<User> { async saveUser(user: User, processValidation = true): Promise<User> {
try { try {
user.updated = new Date(); user.updated = new Date();
if (user.id) { if (user.id) {
@ -125,7 +124,10 @@ export class UserService {
} else { } else {
user.created = new Date(); user.created = new Date();
} }
const validatedUser = UserSchema.parse(user); let validatedUser = user;
if (processValidation) {
validatedUser = UserSchema.parse(user);
}
const drizzleUser = convertUserToDrizzleUser(validatedUser); const drizzleUser = convertUserToDrizzleUser(validatedUser);
if (user.id) { if (user.id) {
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning(); const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
@ -135,15 +137,6 @@ export class UserService {
return convertDrizzleUserToUser(newUser) as User; return convertDrizzleUserToUser(newUser) as User;
} }
} catch (error) { } catch (error) {
if (error instanceof ZodError) {
const filteredErrors = error.errors
.map(item => ({
...item,
field: item.path[0],
}))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
}
throw error; throw error;
} }
} }

View File

@ -7,15 +7,22 @@ import { LoadingService } from '../services/loading.service';
@Injectable() @Injectable()
export class LoadingInterceptor implements HttpInterceptor { export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {} constructor(private loadingService: LoadingService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const hideLoading = request.headers.get('X-Hide-Loading') === 'true';
const requestId = `HTTP-${v4()}`; const requestId = `HTTP-${v4()}`;
if (!hideLoading) {
this.loadingService.startLoading(requestId, request.url); this.loadingService.startLoading(requestId, request.url);
}
return next.handle(request).pipe( return next.handle(request).pipe(
tap({ tap({
finalize: () => this.loadingService.stopLoading(requestId), // Stoppt den Ladevorgang, wenn die Anfrage abgeschlossen ist finalize: () => {
// Beachte, dass 'error' und 'complete' hier entfernt wurden, da 'finalize' in allen Fällen aufgerufen wird, if (!hideLoading) {
// egal ob die Anfrage erfolgreich war, einen Fehler geworfen hat oder abgeschlossen wurde. this.loadingService.stopLoading(requestId);
}
},
}), }),
); );
} }

View File

@ -4,7 +4,7 @@
@if(user){ @if(user){
<a routerLink="/account" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Account</a> <a routerLink="/account" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Account</a>
} @else { } @else {
<a routerLink="/pricing" class="text-gray-800">Pricing</a> <!-- <a routerLink="/pricing" class="text-gray-800">Pricing</a> -->
<a (click)="login()" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Log In</a> <a (click)="login()" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Log In</a>
<a routerLink="/pricing" class="text-white bg-blue-600 px-4 py-2 rounded">Register</a> <a routerLink="/pricing" class="text-white bg-blue-600 px-4 py-2 rounded">Register</a>
} }

View File

@ -43,7 +43,9 @@ export class PricingComponent {
} }
} else { } else {
if (priceId) { if (priceId) {
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}` }); this.keycloakService.register({
redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}`,
});
} else { } else {
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` }); this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
} }

View File

@ -243,9 +243,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-start">
<button routerLink="/pricing" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Upgrade Subscription Plan</button>
</div>
<div class="mt-8 sm:hidden"> <div class="mt-8 sm:hidden">
<h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3> <h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3>
<div class="space-y-2"> <div class="space-y-2">
@ -279,6 +277,16 @@
} }
</div> </div>
</div> </div>
@if(user.subscriptionPlan==='free'){
<div class="flex justify-start">
<button
routerLink="/pricing"
class="py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
Upgrade Subscription Plan
</button>
</div>
}
</div> </div>
} }
</div> </div>

View File

@ -1,6 +1,6 @@
import { DatePipe, TitleCasePipe } from '@angular/common'; import { DatePipe, TitleCasePipe } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { initFlowbite } from 'flowbite'; import { initFlowbite } from 'flowbite';
@ -32,7 +32,7 @@ import { SharedService } from '../../../services/shared.service';
import { SubscriptionsService } from '../../../services/subscriptions.service'; import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils'; import { checkAndUpdate, map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults'; import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
@ -93,6 +93,7 @@ export class AccountComponent {
private validationMessagesService: ValidationMessagesService, private validationMessagesService: ValidationMessagesService,
private subscriptionService: SubscriptionsService, private subscriptionService: SubscriptionsService,
private datePipe: DatePipe, private datePipe: DatePipe,
private router: Router,
) {} ) {}
async ngOnInit() { async ngOnInit() {
setTimeout(() => { setTimeout(() => {
@ -108,9 +109,11 @@ export class AccountComponent {
} }
this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email)); this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
if (this.subscriptions.length === 0) { await this.synchronizeSubscriptions(this.subscriptions);
this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }]; // if (this.subscriptions.length === 0) {
} // this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
// }
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
@ -128,6 +131,36 @@ export class AccountComponent {
label: this.titleCasePipe.transform(type.name), label: this.titleCasePipe.transform(type.name),
})); }));
} }
async synchronizeSubscriptions(subscriptions: StripeSubscription[]) {
let changed = false;
if (this.subscriptions.length === 0) {
if (!this.user.subscriptionPlan) {
this.router.navigate(['pricing']);
} else {
this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
changed = checkAndUpdate(changed, this.user.customerType !== 'buyer' && this.user.customerType !== 'seller', () => (this.user.customerType = 'buyer'));
changed = checkAndUpdate(changed, !!this.user.customerSubType, () => (this.user.customerSubType = null));
changed = checkAndUpdate(changed, this.user.subscriptionPlan !== 'free', () => (this.user.subscriptionPlan = 'free'));
changed = checkAndUpdate(changed, !!this.user.subscriptionId, () => (this.user.subscriptionId = null));
}
} else {
const subscription = subscriptions[0];
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerSubType !== 'broker', () => (this.user.customerSubType = 'broker'));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.subscriptionPlan !== 'broker', () => (this.user.subscriptionPlan = 'broker'));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && !this.user.subscriptionId, () => (this.user.subscriptionId = subscription.id));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionPlan !== 'professional', () => (this.user.subscriptionPlan = 'professional'));
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionId !== 'professional', () => (this.user.subscriptionId = subscription.id));
}
if (changed) {
await this.userService.saveGuaranteed(this.user);
this.cdref.detectChanges();
this.cdref.markForCheck();
}
}
ngOnDestroy() { ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
} }

View File

@ -1,7 +1,15 @@
<div class="min-h-screen flex items-center justify-center"> <div class="min-h-screen flex items-center justify-center">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-md"> <div class="bg-white p-6 rounded-lg shadow-lg max-w-md">
<div class="text-center"> <div class="text-center">
@if(user && (user.subscriptionPlan==='professional' || user.subscriptionPlan==='broker')){ <!-- Neue Bedingung hinzufügen -->
@if(maxAttemptsReached) {
<h2 class="text-2xl font-bold text-red-800 mt-4">We're sorry!</h2>
<p class="text-gray-600 mt-2">
We regret to inform you that we have not yet received any response from our payment service provider regarding the status of your subscription. Please log in to the
<a href="https://www.bizmatch.net" class="text-blue-500 hover:underline">website</a> and check your subscription status under the Account menu. If you have any questions, please contact us at
<a href="mailto:support@bizmatch.net" class="text-blue-500 hover:underline">support&#64;bizmatch.net</a>.
</p>
} @else if(user && (user.subscriptionPlan==='professional' || user.subscriptionPlan==='broker')) {
<svg class="mx-auto h-16 w-16 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="mx-auto h-16 w-16 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg> </svg>

View File

@ -16,7 +16,10 @@ import { map2User } from '../../utils/utils';
}) })
export class SuccessComponent { export class SuccessComponent {
user: User; user: User;
maxAttemptsReached: boolean = false; // Neue Variable hinzufügen
constructor(private keycloakService: KeycloakService, private userService: UserService, private logService: LogService, private router: Router) {} constructor(private keycloakService: KeycloakService, private userService: UserService, private logService: LogService, private router: Router) {}
async ngOnInit() { async ngOnInit() {
let email = null; let email = null;
try { try {
@ -29,6 +32,7 @@ export class SuccessComponent {
this.checkSubscriptionPlan(email, e.message); this.checkSubscriptionPlan(email, e.message);
} }
} }
async checkSubscriptionPlan(email: string, error?: string) { async checkSubscriptionPlan(email: string, error?: string) {
if (!email) { if (!email) {
this.logService.log({ severity: 'error', text: `Unauthorized Access to Success Page ${error}` }); this.logService.log({ severity: 'error', text: `Unauthorized Access to Success Page ${error}` });
@ -44,12 +48,13 @@ export class SuccessComponent {
if (attempts >= maxAttempts) { if (attempts >= maxAttempts) {
clearInterval(intervalId); clearInterval(intervalId);
console.error('Max attempts reached'); console.error('Max attempts reached');
this.maxAttemptsReached = true; // Setze die Variable auf true, wenn die max. Versuche erreicht wurden
return; return;
} }
attempts++; attempts++;
this.user = await this.userService.getByMail(email); this.user = await this.userService.getByMail(email, true);
if (this.user && this.user.subscriptionPlan) { if (this.user && this.user.subscriptionPlan) {
clearInterval(intervalId); clearInterval(intervalId);

View File

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { lastValueFrom, Observable } from 'rxjs'; import { lastValueFrom, Observable } from 'rxjs';
import urlcat from 'urlcat'; import urlcat from 'urlcat';
@ -20,12 +20,19 @@ export class UserService {
async save(user: User): Promise<User> { async save(user: User): Promise<User> {
return await lastValueFrom(this.http.post<User>(`${this.apiBaseUrl}/bizmatch/user`, user)); return await lastValueFrom(this.http.post<User>(`${this.apiBaseUrl}/bizmatch/user`, user));
} }
async saveGuaranteed(user: User): Promise<User> {
return await lastValueFrom(this.http.post<User>(`${this.apiBaseUrl}/bizmatch/user/guaranteed`, user));
}
async getById(id: string): Promise<User> { async getById(id: string): Promise<User> {
return await lastValueFrom(this.http.get<User>(`${this.apiBaseUrl}/bizmatch/user/${id}`)); return await lastValueFrom(this.http.get<User>(`${this.apiBaseUrl}/bizmatch/user/${id}`));
} }
async getByMail(mail: string): Promise<User> { async getByMail(mail: string, hideLoading: boolean = true): Promise<User> {
const url = urlcat(`${this.apiBaseUrl}/bizmatch/user`, { mail }); const url = urlcat(`${this.apiBaseUrl}/bizmatch/user`, { mail });
return await lastValueFrom(this.http.get<User>(url)); let headers = new HttpHeaders();
if (hideLoading) {
headers = headers.set('X-Hide-Loading', 'true');
}
return await lastValueFrom(this.http.get<User>(url, { headers }));
} }
async search(criteria?: UserListingCriteria): Promise<ResponseUsersArray> { async search(criteria?: UserListingCriteria): Promise<ResponseUsersArray> {
return await lastValueFrom(this.http.post<ResponseUsersArray>(`${this.apiBaseUrl}/bizmatch/user/search`, criteria)); return await lastValueFrom(this.http.post<ResponseUsersArray>(`${this.apiBaseUrl}/bizmatch/user/search`, criteria));

View File

@ -287,6 +287,12 @@ export function assignProperties(target, source) {
} }
return target; return target;
} }
export function checkAndUpdate(changed: boolean, condition: boolean, assignment: () => any): boolean {
if (condition) {
assignment();
}
return changed || condition;
}
// ----------------------------- // -----------------------------
// Criteria Proxy // Criteria Proxy
// ----------------------------- // -----------------------------