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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 { // 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 { // 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 { 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 { 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 { 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()); } } }