Mail Modul überarbeitet, Korrekturen am PaymentService, neuer customerType seller

This commit is contained in:
Andreas Knuth 2024-08-22 22:59:33 +02:00
parent b4609d07ba
commit 7a286e3519
16 changed files with 183 additions and 76 deletions

View File

@ -14,7 +14,8 @@
"console": "integratedTerminal",
"env": {
"HOST_NAME": "localhost"
}
},
"preLaunchTask": "Start Stripe Listener"
},
{
"type": "node",
@ -60,5 +61,30 @@
"sourceMaps": true,
"smartStep": true
}
],
"tasks": [
{
"label": "Start Stripe Listener",
"type": "shell",
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
"isBackground": true,
"problemMatcher": [
{
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
]
}
]
}

31
bizmatch-server/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,31 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Stripe Listener",
"type": "shell",
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
"problemMatcher": [],
"isBackground": true, // Task läuft im Hintergrund
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "Start Nest.js",
"type": "npm",
"script": "start:debug",
"isBackground": false,
"problemMatcher": [],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
}
]
}

View File

@ -2,7 +2,7 @@ import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial
import { AreasServed, LicensedIn } from '../models/db.model';
export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'seller', 'professional']);
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']);
@ -33,8 +33,6 @@ export const users = pgTable('users', {
longitude: doublePrecision('longitude'),
stripeCustomerId: text('stripeCustomerId'),
subscriptionId: text('subscriptionId'),
planActive: boolean('planActive').default(false),
planExpires: timestamp('planExpires'),
subscriptionPlan: subscriptionTypeEnum('subscriptionType'),
// embedding: vector('embedding', { dimensions: 1536 }),
});

View File

@ -0,0 +1,14 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
host: 'email-smtp.us-east-2.amazonaws.com',
port: 587,
secure: false,
auth: {
user: process.env.AMAZON_USER,
pass: process.env.AMAZON_PASSWORD,
},
defaults: {
from: '"No Reply" <noreply@example.com>',
},
}));

View File

@ -1,4 +1,5 @@
import { Body, Controller, Post } from '@nestjs/common';
import { User } from 'src/models/db.model';
import { ErrorResponse, MailInfo } from '../models/main.model';
import { MailService } from './mail.service.js';
@ -13,4 +14,8 @@ export class MailController {
return this.mailService.sendRequest(mailInfo);
}
}
@Post('subscriptionConfirmation')
sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
return this.mailService.sendSubscriptionConfirmation(user);
}
}

View File

@ -13,21 +13,50 @@ import { MailController } from './mail.controller.js';
import { MailService } from './mail.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const user = process.env.amazon_user;
const password = process.env.amazon_password;
const user = process.env.AMAZON_USER;
const password = process.env.AMAZON_PASSWORD;
console.log('Amazon User:', process.env.AMAZON_USER);
console.log('Amazon Password:', process.env.AMAZON_PASSWORD);
@Module({
imports: [
DrizzleModule,
UserModule,
GeoModule,
MailerModule.forRoot({
// ConfigModule.forFeature(mailConfig),
// MailerModule.forRoot({
// transport: {
// host: 'email-smtp.us-east-2.amazonaws.com',
// secure: false,
// port: 587,
// auth: {
// user: user, //'AKIAU6GDWVAQ2QNFLNWN',
// pass: password, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
// },
// },
// defaults: {
// from: '"No Reply" <noreply@example.com>',
// },
// template: {
// dir: join(__dirname, 'templates'),
// adapter: new HandlebarsAdapter({
// eq: function (a, b) {
// return a === b;
// },
// }),
// options: {
// strict: true,
// },
// },
// }),
MailerModule.forRootAsync({
useFactory: () => ({
transport: {
host: 'email-smtp.us-east-2.amazonaws.com',
secure: false,
port: 587,
auth: {
user: 'AKIAU6GDWVAQ2QNFLNWN',
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
user: process.env.AMAZON_USER, //'AKIAU6GDWVAQ2QNFLNWN',
pass: process.env.AMAZON_PASSWORD, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
},
},
defaults: {
@ -35,12 +64,17 @@ const password = process.env.amazon_password;
},
template: {
dir: join(__dirname, 'templates'),
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
adapter: new HandlebarsAdapter({
eq: function (a, b) {
return a === b;
},
}),
options: {
strict: true,
},
},
}),
}),
],
providers: [MailService, UserService, FileService, GeoService],
controllers: [MailController],

View File

@ -83,7 +83,7 @@ export class MailService {
}
async sendSubscriptionConfirmation(user: User): Promise<void> {
await this.mailerService.sendMail({
to: 'support@bizmatch.net',
to: user.email,
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
subject: `Subscription Confirmation`,
//template: './inquiry', // `.hbs` extension is appended automatically

View File

@ -17,18 +17,18 @@ export interface UserData {
hasCompanyLogo?: boolean;
licensedIn?: string[];
gender?: 'male' | 'female';
customerType?: 'buyer' | 'broker' | 'professional';
customerType?: 'buyer' | 'seller' | 'professional';
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
created?: Date;
updated?: Date;
}
export type Gender = 'male' | 'female';
export type CustomerType = 'buyer' | 'professional';
export type CustomerType = 'buyer' | 'seller' | 'professional';
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
export type ListingsCategory = 'commercialProperty' | 'business';
export const GenderEnum = z.enum(['male', 'female']);
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']);
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);

View File

@ -58,23 +58,19 @@ export class PaymentService {
quantity: 1,
},
],
success_url: 'http://localhost:4200/success',
cancel_url: 'http://localhost:4200/pricing',
success_url: `${process.env.WEB_HOST}/success`,
cancel_url: `${process.env.WEB_HOST}/pricing`,
// customer_email: checkout.email,
customer: customerId,
// customer_details:{
// name: checkout.name, // Hier wird der Name des Kunden übergeben
// },
shipping_address_collection: {
allowed_countries: ['US'],
},
client_reference_id: '1234',
client_reference_id: btoa(checkout.name),
locale: 'en',
subscription_data: {
trial_end: Math.floor(new Date().setMonth(new Date().getMonth() + 3) / 1000),
},
metadata: { plan: product.name },
},
});
return session;
} else {
@ -100,20 +96,11 @@ export class PaymentService {
});
user.stripeCustomerId = session.customer as string;
user.subscriptionId = session.subscription as string;
user.planActive = true;
//session.shipping_details ->
// "shipping_details": {
// "address": {
// "city": "Springfield",
// "country": "US",
// "line1": "West Maple Avenue South",
// "line2": null,
// "postal_code": "62704",
// "state": "IL"
// },
// "name": "Johnathan Miller"
// }
user.subscriptionPlan = session.amount_total === 4900 ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
user.customerType = 'professional';
if (session.metadata['plan'] === 'Broker Plan') {
user.customerSubType = 'broker';
}
user.subscriptionPlan = session.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
this.userService.saveUser(user);
this.mailService.sendSubscriptionConfirmation(user);
}

View File

@ -52,6 +52,7 @@ export class SelectOptionsService {
];
public customerTypes: Array<KeyValue> = [
{ name: 'Buyer', value: 'buyer' },
{ name: 'Commercial Property Seller', value: 'seller' },
{ name: 'Professional', value: 'professional' },
];
public customerSubTypes: Array<KeyValue> = [

View File

@ -11,7 +11,15 @@
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage"></app-tooltip>
}
</label>
<select [id]="name" [name]="name" [ngModel]="value" (change)="onSelectChange($event)" (blur)="onTouched()" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<select
[disabled]="disabled"
[id]="name"
[name]="name"
[ngModel]="value"
(change)="onSelectChange($event)"
(blur)="onTouched()"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
>
<option value="" disabled selected>Select an option</option>
<option *ngFor="let option of options" [value]="option.value">
{{ option.label }}

View File

@ -20,6 +20,7 @@ import { ValidationMessagesService } from '../validation-messages.service';
})
export class ValidatedSelectComponent extends BaseInputComponent {
@Input() options: Array<{ value: any; label: string }> = [];
@Input() disabled = false;
@Output() valueChange = new EventEmitter<any>();
constructor(validationMessagesService: ValidationMessagesService) {

View File

@ -39,7 +39,7 @@ export class PricingComponent {
await this.userService.save(user);
this.router.navigate([`/account`]);
} else {
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
this.checkout({ priceId: priceId, email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
}
} else {
if (priceId) {

View File

@ -78,7 +78,7 @@
</div>
}@else{
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
<app-validated-select [disabled]="user.customerType === 'professional'" label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
} @if (isProfessional){
<!-- <div>
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
@ -86,7 +86,7 @@
<option *ngFor="let subType of customerSubTypes" [value]="subType">{{ subType | titlecase }}</option>
</select>
</div> -->
<app-validated-select label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
<app-validated-select [disabled]="user.customerSubType === 'broker'" label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
}
</div>
@if (isProfessional){
@ -246,39 +246,39 @@
<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>
<div class="space-y-2">
@for (subscription of userSubscriptions; track userSubscriptions){
@for (subscription of subscriptions; track subscriptions; let i=$index){
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">Level</dt>
<dd class="text-sm text-gray-900">{{ level }}</dd>
<dd class="text-sm text-gray-900">{{ getLevel(i) }}</dd>
</div>
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">Start Date</dt>
<dd class="text-sm text-gray-900">{{ subscription.start | date }}</dd>
</div>
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">Date Modified</dt>
<dd class="text-sm text-gray-900">{{ subscription.modified | date }}</dd>
<dd class="text-sm text-gray-900">{{ getStartDate(i) }}</dd>
</div>
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">End Date</dt>
<dd class="text-sm text-gray-900">{{ subscription.end | date }}</dd>
<dd class="text-sm text-gray-900">{{ getEndDate(i) }}</dd>
</div>
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">Next Settlement</dt>
<dd class="text-sm text-gray-900">{{ getNextSettlement(i) }}</dd>
</div>
<div class="sm:col-span-1 flex">
<dt class="text-sm font-bold text-gray-500 mr-2">Status</dt>
<dd class="text-sm text-gray-900">{{ subscription.status }}</dd>
<dd class="text-sm text-gray-900">{{ getStatus(i) }}</dd>
</div>
</dl>
</div>
</div>
}
</div>
</div> -->
</div>
</div>
}
</div>

View File

@ -67,8 +67,6 @@ export class AccountComponent {
editorModules = TOOLBAR_OPTIONS;
env = environment;
faTrash = faTrash;
customerTypes = ['buyer', 'professional'];
customerSubTypes = ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser'];
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
@ -116,14 +114,18 @@ export class AccountComponent {
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.customerTypeOptions = this.customerTypes.map(type => ({
value: type,
label: this.titleCasePipe.transform(type),
this.customerTypeOptions = this.selectOptions.customerTypes
.filter(ct => ct.value === 'buyer' || ct.value === 'seller' || this.user.customerType === 'professional')
.map(type => ({
value: type.value,
label: this.titleCasePipe.transform(type.name),
}));
this.customerSubTypeOptions = this.customerSubTypes.map(type => ({
value: type,
label: this.titleCasePipe.transform(type),
this.customerSubTypeOptions = this.selectOptions.customerSubTypes
.filter(ct => ct.value !== 'broker' || this.user.customerSubType === 'broker')
.map(type => ({
value: type.value,
label: this.titleCasePipe.transform(type.name),
}));
}
ngOnDestroy() {

View File

@ -37,7 +37,7 @@ export class SuccessComponent {
}
let attempts = 0;
const maxAttempts = 5;
const maxAttempts = 20;
const interval = 5000; // 5 Sekunden
const intervalId = setInterval(async () => {