agmission/Development/client/src/app/accounts/effects/account.effects.ts

278 lines
11 KiB
TypeScript

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, of } from 'rxjs';
import { map, switchMap, catchError, repeat } from 'rxjs/operators';
import { Action } from '@ngrx/store';
import * as userActions from '../actions/account.actions';
import { UserService } from '@app/domain/services/user.service';
import { AuthService } from '@app/domain/services/auth.service';
import { AppMessageService } from '@app/shared/app-message.service';
import { PartnerService } from '@app/partners/services/partner.service';
import { PartnerSystemUser } from '@app/accounts/models/user.model';
import { RoleIds, globals, KnownPartnerCodes } from '@app/shared/global';
@Injectable()
export class AccountEffects {
constructor(
private readonly actions$: Actions,
private readonly userSvc: UserService,
private readonly authSvc: AuthService,
private readonly msgSvc: AppMessageService,
private readonly partnerSvc: PartnerService
) {
}
@Effect()
loadUsers$: Observable<Action> = this.actions$.pipe(
ofType<userActions.Fetch>(userActions.FETCH),
switchMap(() =>
// All account types (including PARTNER_SYSTEM_USER) are returned by the backend
// /api/users/search endpoint — no separate /api/partners/systemUsers call needed.
this.userSvc.loadUsers({ byPuid: this.authSvc.user.parent }).pipe(
map(users => new userActions.FetchSuccess(users))
)
),
catchError(err => this.handleUserOperationError(err, 'load')),
repeat()
);
@Effect()
createUser$: Observable<Action> = this.actions$.pipe(
ofType<userActions.Create>(userActions.CREATE),
switchMap(({ payload }) => {
// Extract user data and partner config from payload
const { partnerConfig, ...userData } = payload;
// For partner system users, create them directly through PartnerService
if (partnerConfig && partnerConfig.vendorSystemType) {
return this.createPartnerSystemUser(userData, partnerConfig);
}
// For regular users, use UserService directly
return this.userSvc.saveUser(userData).pipe(
map((savedUser) => new userActions.CreateSuccess(savedUser))
);
}),
catchError(err => this.handleUserOperationError(err, 'create')),
repeat()
);
@Effect()
updateUser$: Observable<Action> = this.actions$.pipe(
ofType<userActions.Update>(userActions.UPDATE),
switchMap(({ payload }) => {
// Extract user data and partner config from payload
const { partnerConfig, ...userData } = payload;
// Case 1: User WITHOUT partner - use UserService directly + cleanup
if (!partnerConfig || !partnerConfig.vendorSystemType) {
return this.userSvc.saveUser(userData).pipe(
switchMap((savedUser) => {
// Clean up any existing partner system users for non-partner accounts
return this.cleanupPartnerSystemUsers(userData._id).pipe(
map(() => new userActions.UpdateSuccess(savedUser)),
catchError(err => {
console.error('Partner cleanup failed:', err);
// User update succeeded, cleanup failed is not critical
return of(new userActions.UpdateSuccess(savedUser));
})
);
})
);
}
// Case 2: User WITH partner - use PartnerService workflow completely
return this.updatePartnerUserWorkflow(userData, partnerConfig).pipe(
map((savedUser) => new userActions.UpdateSuccess(savedUser))
);
}),
catchError(err => this.handleUserOperationError(err, 'save')),
repeat()
);
@Effect()
deleteUser$: Observable<Action> = this.actions$.pipe(
ofType<userActions.Delete>(userActions.DELETE),
switchMap(({ payload }) => {
// Check if the user is a PARTNER_SYSTEM_USER
if (payload.kind === RoleIds.PARTNER_SYSTEM_USER) {
// Backend only disables partner system users (sets active=false), it does NOT remove them.
// Dispatch UpdateSuccess so the store reflects the disabled state in-place rather than
// removing the row — which would cause it to reappear on the next reload.
return this.partnerSvc.deleteSystemUser(payload._id).pipe(
map(() => new userActions.UpdateSuccess({ ...payload, active: false }))
);
} else {
// Use UserService for regular users
return this.userSvc.deleteUser(payload).pipe(
map(() => new userActions.DeleteSuccess(payload))
);
}
}),
catchError(err => this.handleUserOperationError(err, 'delete')),
repeat()
);
// Partner user workflow methods - use PartnerService exclusively
private createPartnerSystemUser(userData: any, partnerConfig: any): Observable<Action> {
// Get partner ID based on vendor type
return this.getPartnerByVendorType(partnerConfig.vendorSystemType).pipe(
switchMap(partnerId => {
if (!partnerId) {
throw new Error(`Failed to get partner for vendor type: ${partnerConfig.vendorSystemType}`);
}
// Create vendor-specific system user data
const createData = this.buildPartnerSystemUserData(userData, partnerConfig, partnerId);
return this.partnerSvc.createSystemUser(createData).pipe(
map((systemUser) => {
// ✅ FIX: Return the created system user with customerId/partnerId for post-save validation
// Merge the saved systemUser data with original userData to preserve all fields
return new userActions.CreateSuccess({
...userData,
...systemUser,
// Ensure we have the IDs for post-save validation
customer: systemUser.customer || createData.customerId,
partner: systemUser.partner || createData.partnerId
});
})
);
})
);
}
private updatePartnerUserWorkflow(userData: any, partnerConfig: any): Observable<any> {
// Use getSystemUserById to directly fetch the partner system user
return this.partnerSvc.getSystemUserById(userData._id).pipe(
switchMap(existingSystemUser => {
if (existingSystemUser) {
// Update existing partner system user with backend-compatible structure
const updateData = this.buildPartnerSystemUserData(userData, partnerConfig, existingSystemUser.partner._id);
return this.partnerSvc.updateSystemUser(existingSystemUser._id!, updateData).pipe(
map(() => userData) // Return the user data
);
} else {
// Partner system user doesn't exist, return error
throw new Error('Partner system user not found for update');
}
})
);
}
/**
* Build partner system user data structure based on vendor type
* This method can be extended to support additional vendors
*/
private buildPartnerSystemUserData(userData: any, partnerConfig: any, partnerId: string): any {
return {
partnerId: partnerId,
customerId: userData.parent, // AgMission customer (main applicator account)
username: userData.username,
password: userData.password,
name: userData.name,
active: userData.active,
email: userData.email,
address: userData.address,
phone: userData.phone,
companyId: partnerConfig.vendorConfiguration.companyId || null,
apiKey: partnerConfig.vendorConfiguration.apiKey || null,
apiSecret: partnerConfig.vendorConfiguration.apiSecret || null
// NOTE: metadata intentionally omitted — partner identity is carried by
// partnerId (ObjectId). metadata.vendor was a fragile frontend-derived
// copy that could silently diverge from the partner document.
};
}
/**
* Get partner ID by vendor type
* This method can be extended to support additional vendors
*/
private getPartnerByVendorType(vendorType: string): Observable<string | null> {
return this.partnerSvc.getPartners().pipe(
map((partners: any[]) => {
let partner = null;
switch (vendorType) {
case KnownPartnerCodes.SATLOC:
partner = partners.find(p =>
p.partnerCode === KnownPartnerCodes.SATLOC.toUpperCase() ||
p.name?.toLowerCase().includes(KnownPartnerCodes.SATLOC)
);
break;
// Add additional vendors here as needed
// case 'other_vendor':
// partner = partners.find(p =>
// p.partnerCode === 'OTHER_VENDOR' ||
// p.name?.toLowerCase().includes('other_vendor')
// );
// break;
default:
// Fallback: try to find partner by name or code matching vendor type
partner = partners.find(p =>
p.partnerCode?.toLowerCase() === vendorType.toLowerCase() ||
p.name?.toLowerCase().includes(vendorType.toLowerCase())
);
break;
}
return partner ? partner._id : null;
}),
catchError(() => of(null))
);
}
private cleanupPartnerSystemUsers(userId: string): Observable<any> {
return this.partnerSvc.getSystemUsersForCustomer(userId).pipe(
switchMap((systemUsers: PartnerSystemUser[]) => {
if (systemUsers.length === 0) {
return of(null);
}
// Delete all system users for this customer
const deleteOperations = systemUsers.map(systemUser =>
this.partnerSvc.deleteSystemUser(systemUser._id!).pipe(
catchError(error => {
console.error('Failed to delete partner system user:', error);
return of(null);
})
)
);
// Wait for all delete operations to complete
return of(...deleteOperations);
}),
catchError(error => {
console.error('Failed to load partner system users for cleanup:', error);
return of(null);
})
);
}
// Centralized error handler for user operations following subscription.effects pattern
private handleUserOperationError(err: any, operation: 'create' | 'save' | 'delete' | 'load'): Observable<Action> {
const actionVerb = operation === 'create' ? globals.create :
operation === 'save' ? globals.save :
operation === 'delete' ? globals.delete : globals.load;
// For load operation, use 'accounts' (plural), for others use 'account' (singular)
const thingName = operation === 'load' ? globals.accounts : globals.account;
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', actionVerb).replace('#thing#', thingName));
if (operation === 'create') {
return of(new userActions.CreateFailed());
} else if (operation === 'save') {
return of(new userActions.UpdateFailed());
} else if (operation === 'delete') {
return of(new userActions.UpdateFailed()); // Note: There's no DeleteFailed action, using UpdateFailed
} else {
return of(new userActions.FetchError());
}
}
}