using AccountsDAL.CustomCode.Account; using AccountsDAL.CustomCode.Party; using AccountsDAL.CustomCode.PartyBranch; using AccountsDAL.CustomCode.PartyOU; using AccountsDAL.DTO.Account; using AccountsDAL.DTO.Party; using AccountsDAL.DTO.PartyOU; using AccountsDAL.CustomCode.Address; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using Newtonsoft.Json; using System.Data.Common; using static GB5Shared.GB5Constant.Constant; namespace AccountsBLL.Party { public class PartyBLL : IPartyBLL { private readonly IPartyDAL _partyDAL; private readonly IPartyBranchDAL _partyBranchDAL; private readonly IPartyOUDAL _partyOUDAL; private readonly IAddressDAL _addressDAL; private readonly IAccountDAL _accountDAL; private readonly AutoNumber _autoNumber; private readonly CodeGeneration _codeGeneration; private readonly IQueryExecutor _queryExecutor; public PartyBLL( IPartyDAL partyDAL, IPartyBranchDAL partyBranchDAL, IPartyOUDAL partyOUDAL, IAddressDAL addressDAL, IAccountDAL accountDAL, AutoNumber autoNumber, CodeGeneration codeGeneration, IQueryExecutor queryExecutor) { _partyDAL = partyDAL; _partyBranchDAL = partyBranchDAL; _partyOUDAL = partyOUDAL; _addressDAL = addressDAL; _accountDAL = accountDAL; _autoNumber = autoNumber; _codeGeneration = codeGeneration; _queryExecutor = queryExecutor; } // Enriches the raw Party+PartyBranch rows with the branch's embedded Address (loaded // separately, matching legacy GB4's GetPartyNew, which fetches Address via // AddressDAL.GetAddress(ObjectTypeId=OBJECTPARTYBRANCH, ObjectId=PartyBranchId) rather // than joining it into the branch SELECT) and the party's PartyOU list. Unlike legacy, // which filters BulkPostPartyOUDTOs down to a single branch, this returns every PartyOU // row across all of the party's branches since GB5's PartyDTO isn't restricted to one branch. public async Task GetParty(int partyId, LoginDTO loginDTO) { var dto = await _partyDAL.GetPartyDTO(partyId, loginDTO).ConfigureAwait(false); if (dto == null) return JsonConvert.SerializeObject(dto); foreach (var branch in dto.PartyBranchList) { var address = await _addressDAL .FindAddress(EntityConstant.OBJECTPARTYBRANCH, branch.PartyBranchId, loginDTO) .ConfigureAwait(false); if (address != null) branch.AddressDTO = MapAddressToReport(address); // Configurable (MADDONFIELDS-driven) addon block — one row per branch. branch.PartyBranchAddon = await _partyBranchDAL.GetPartyBranchAddon(branch.PartyBranchId, loginDTO).ConfigureAwait(false); } dto.BulkPostPartyOUDTOs = await _partyOUDAL.GetPartyOUsForParty(partyId, loginDTO).ConfigureAwait(false); // Legacy clients read the primary branch off the singular PartyBranchDTO field, not // PartyBranchList — mirror the first branch back into it so Get matches GB4's shape. if (dto.PartyBranchList.Count > 0) dto.PartyBranchDTO = dto.PartyBranchList[0]; return JsonConvert.SerializeObject(dto); } public Task GetSelectListParty(int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) => _partyDAL.GetSelectListParty(firstNumber, maxResult, criteriaDTO, loginDTO); public async Task SaveParty(PartyDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); if (string.IsNullOrWhiteSpace(dto.PartyName)) throw new ArgumentException("Party name is required."); if (dto.CurrencyId == 0) throw new ArgumentException("Currency is required."); if (dto.PartyBranchDTO != null && dto.PartyBranchList.Count == 0) { dto.PartyBranchList.Add((AccountsDAL.DTO.PartyBranch.PartyBranchDTO)dto.PartyBranchDTO); } var tx = await _queryExecutor.BeginTransactionAsync(loginDTO).ConfigureAwait(false); try { dto.TenantId = loginDTO.ClientId; bool isNew = dto.PartyId == 0; if (isNew) { var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.PARTY, loginDTO).ConfigureAwait(false); dto.PartyId = auto.StartNumber; dto.PartyVersion = 1; // Auto-generated codes are assigned once, at creation, never regenerated on // update — AssignCode has no isNew-awareness itself (it just re-runs the Auto // GENERATIONTYPE path every call), so it must only be invoked here. dto = await _codeGeneration.AssignCode("PARTY", dto, loginDTO).ConfigureAwait(false); } else { dto.PartyVersion++; } await LinkPartyAccountAsync(dto, loginDTO, tx).ConfigureAwait(false); dto.PartySourceType = loginDTO.SourceType; if (isNew) await _partyDAL.SaveParty(dto, loginDTO, tx).ConfigureAwait(false); else await _partyDAL.UpdateParty(dto, loginDTO, tx).ConfigureAwait(false); foreach (var branch in dto.PartyBranchList) { branch.PartyId = dto.PartyId; bool isNewBranch = branch.PartyBranchId == 0; if (isNewBranch) { var branchAuto = await _autoNumber.GetNumberAsync(1, "PARTYBRANCH", loginDTO).ConfigureAwait(false); branch.PartyBranchId = branchAuto.StartNumber; branch.PartyBranchVersion = 1; } else { branch.PartyBranchVersion++; } branch.PartyBranchSourceType = loginDTO.SourceType; if (branch.AddressDTO != null) branch.DefaultAddressId = await SaveBranchAddressAsync(branch, loginDTO, tx).ConfigureAwait(false); // PartyBranchCode is manual-entry only — confirmed against GB4, which never // calls its code-generation service for PARTYBRANCH (only for PARTY/CONTACT). // Persist whatever code the caller supplied as-is. if (isNewBranch) await _partyBranchDAL.SavePartyBranch(branch, loginDTO, tx).ConfigureAwait(false); else await _partyBranchDAL.UpdatePartyBranch(branch, loginDTO, tx).ConfigureAwait(false); var branchAddon = branch.PartyBranchAddon?.FirstOrDefault(); if (branchAddon != null && !string.IsNullOrWhiteSpace(branchAddon.FeAddon)) await _partyBranchDAL.SavePartyBranchAddon(branch.PartyBranchId, branchAddon.FeAddon, loginDTO, tx).ConfigureAwait(false); foreach (var ou in dto.BulkPostPartyOUDTOs) await SavePartyOUAsync(dto, branch, ou, loginDTO, tx).ConfigureAwait(false); } await _queryExecutor.CommitAsync(tx).ConfigureAwait(false); return isNew ? SuccessResponse.SaveSuccess : SuccessResponse.UpdateSuccess; } catch { await _queryExecutor.RollbackAsync(tx).ConfigureAwait(false); throw; } } //public Task SaveParty(PartyDTO dto, LoginDTO loginDTO, CancellationToken ct) // => SaveParty(dto, loginDTO); // Legacy GB4 PartyBLL.NewPartySaveTuned ("AccountSave Fields Assigning" region): MPARTY has // no TDS/Bank columns of its own — for a normal Party (PartyIsAccount == 0) GB4 auto-creates a // dedicated MACCOUNT sub-ledger row sharing the Party's own id (AccountId == PartyId, // ParentId == ControlAccountId, AccountType = 3) to hold them, then points AccountLinkId at // it before the Party row itself is saved. When PartyIsAccount != 0 the Party already points // at an account of the caller's choosing, so this is skipped entirely, matching GB4. private async Task LinkPartyAccountAsync(PartyDTO dto, LoginDTO loginDTO, DbTransaction tx) { if (dto.PartyIsAccount != 0) return; var account = new AccountDTO { AccountId = dto.PartyId, AccountCode = dto.PartyCode, AccountName = dto.PartyName, AccountShortName = dto.PartyShortName, AccountType = 3, AccountGroupId = dto.AccountGroupId, ParentId = dto.ControlAccountId, CurrencyId = dto.CurrencyId, AccountFavouringName = dto.PartyFavouringName, AccountBankBranchId = dto.AccountBankBranchId, TDSCategoryId = dto.TDSCategoryId, AccountIsCompany = dto.AccountIsCompany, AccountIsTDSApplicable = dto.AccountIsTDSApplicable, AccountTDSState = dto.AccountTDSState, AccountBankAccountNumber = dto.AccountBankAccountNumber, AccountBankAccountType = dto.AccountBankAccountType, AccountIsCreditAllowed = 1, AccountIsDebitAllowed = 1, AccountSortOrder = 9999, AccountStatus = dto.PartyStatus, AccountSourceType = loginDTO.SourceType, CodeDefineId = -1, DefaultAddressId = -1, BaseTenantId = loginDTO.ClientId, }; bool accountExists = await _accountDAL.AccountExists(dto.PartyId, loginDTO, tx).ConfigureAwait(false); if (!accountExists) await _accountDAL.SaveAccount(account, loginDTO, tx).ConfigureAwait(false); else await _accountDAL.UpdateAccount(account, loginDTO, tx).ConfigureAwait(false); dto.AccountLinkId = dto.PartyId; } // PartyBranch addresses are stored in MADDRESS keyed by ObjectTypeId=OBJECTPARTYBRANCH / ObjectId=PartyBranchId // (legacy GB4 PartyBranchBLL.saveAddress convention) — the resulting AddressId feeds back into // PartyBranchDTO.DefaultAddressId. Party itself never owns an address directly. // Insert-vs-update is decided by looking up an existing row for this branch, NOT by whether // the caller sent AddressId=0 — clients routinely omit/reset AddressId on every request, and // trusting that blindly created a duplicate MADDRESS row (and a broken Get) on every re-save. private async Task SaveBranchAddressAsync( AccountsDAL.DTO.PartyBranch.PartyBranchDTO branch, LoginDTO loginDTO, DbTransaction tx) { var address = MapAddress(branch.AddressDTO); var existing = await _addressDAL .FindAddress(EntityConstant.OBJECTPARTYBRANCH, branch.PartyBranchId, loginDTO, tx) .ConfigureAwait(false); bool isNewAddress = existing == null; if (isNewAddress) { var addressAuto = await _autoNumber.GetNumberAsync(1, "ADDRESS", loginDTO).ConfigureAwait(false); address.AddressId = addressAuto.StartNumber; } else { address.AddressId = existing!.AddressId; } address.AddressObjectTypeId = EntityConstant.OBJECTPARTYBRANCH; address.AddressObjectId = branch.PartyBranchId; address.AddressSourceType = loginDTO.SourceType; address.AddressStatus = 1; address.AddressCreatedById = loginDTO.UserId; address.AddressCreatedOn = DateTime.UtcNow; address.AddressModifiedById = loginDTO.UserId; address.AddressModifiedOn = DateTime.UtcNow; if (isNewAddress) await _addressDAL.SaveAddress(address, "EN", loginDTO, tx).ConfigureAwait(false); else await _addressDAL.UpdateAddress(address, loginDTO, tx).ConfigureAwait(false); return address.AddressId; } private static AccountsDAL.DTO.Address.AddressDTO MapAddress(GB5Shared.DTO.Report.AddressDTO src) => new() { AddressId = src.AddressId, AddressLine1 = src.AddressLine1, AddressLine2 = src.AddressLine2, AddressLine3 = src.AddressLine3, AddressLine4 = src.AddressLine4, AddressLine5 = src.AddressLine5, AddressZipCode = src.AddressZipCode, CityId = src.CityId, CountryId = src.CountryId, AddressPhone = src.AddressPhone, AddressMobile = src.AddressMobile, AddressFax = src.AddressFax, AddressWeb = src.AddressWeb, AddressGeoCode = src.AddressGeoCode, AddressMail = src.AddressMail, AddressVATTINNumber = src.AddressVATTINNumber, AddressVATCSTNumber = src.AddressVATCSTNumber, AddressPAN = src.AddressPAN, AddressTAN = src.AddressTAN, AddressServiceTaxNumber = src.AddressServiceTaxNumber, AddressExciseDivision = src.AddressExciseDivision, AddressExciseRange = src.AddressExciseRange, AddressExciseCodeNumber = src.AddressExciseCodeNumber, AddressExciseNotification = src.AddressExciseNotification, AddressExciseCircle = src.AddressExciseCircle, AddressExciseCollectrate = src.AddressExciseCollectrate, AddressExciseRegisterNumber = src.AddressExciseRegisterNumber, AddressTDSCircle = src.AddressTDSCircle, StandardRegionId = src.StandardRegionId, LocationId = src.LocationId, AddressGSTIn = src.AddressGSTIn, StateId = src.StateId, AddressGeolocation = src.AddressGeolocation, AddressBorder = src.AddressBorder, AddressSortOrder = src.AddressSortOrder, AddressVersion = src.AddressVersion, }; private static GB5Shared.DTO.Report.AddressDTO MapAddressToReport(AccountsDAL.DTO.Address.AddressDTO src) => new() { AddressId = src.AddressId, AddressObjectTypeId = src.AddressObjectTypeId, AddressObjectId = src.AddressObjectId, AddressAddressType = src.AddressAddressType, AddressAddressNature = src.AddressAddressNature, AddressLine1 = src.AddressLine1, AddressLine2 = src.AddressLine2, AddressLine3 = src.AddressLine3, AddressLine4 = src.AddressLine4, AddressLine5 = src.AddressLine5, AddressZipCode = src.AddressZipCode, CityId = src.CityId, CityCode = src.CityCode, CityName = src.CityName, CountryId = src.CountryId, CountryCode = src.CountryCode, CountryName = src.CountryName, CountryISDCode = src.CountryISDCode, AddressPhone = src.AddressPhone, AddressMobile = src.AddressMobile, AddressFax = src.AddressFax, AddressWeb = src.AddressWeb, AddressGeoCode = src.AddressGeoCode, AddressMail = src.AddressMail, AddressVATTINNumber = src.AddressVATTINNumber, AddressVATCSTNumber = src.AddressVATCSTNumber, AddressPAN = src.AddressPAN, AddressTAN = src.AddressTAN, AddressServiceTaxNumber = src.AddressServiceTaxNumber, AddressExciseDivision = src.AddressExciseDivision, AddressExciseRange = src.AddressExciseRange, AddressExciseCodeNumber = src.AddressExciseCodeNumber, AddressExciseNotification = src.AddressExciseNotification, AddressExciseCircle = src.AddressExciseCircle, AddressExciseCollectrate = src.AddressExciseCollectrate, AddressExciseRegisterNumber = src.AddressExciseRegisterNumber, AddressTDSCircle = src.AddressTDSCircle, StandardRegionId = src.StandardRegionId, StandardRegionCode = src.StandardRegionCode, StandardRegionName = src.StandardRegionName, LocationId = src.LocationId, LocationCode = src.LocationCode, LocationName = src.LocationName, AddressGSTIn = src.AddressGSTIn, StateId = src.StateId, StateCode = src.StateCode, StateName = src.StateName, StateGSTStateCode = src.StateGSTStateCode, StateTINStart = src.StateTINStart, AddressGeolocation = src.AddressGeolocation, AddressBorder = src.AddressBorder, AddressCreatedById = src.AddressCreatedById, AddressCreatedOn = src.AddressCreatedOn, AddressModifiedById = src.AddressModifiedById, AddressModifiedOn = src.AddressModifiedOn, AddressSortOrder = src.AddressSortOrder, AddressVersion = src.AddressVersion, AddressSourceType = src.AddressSourceType, AddressStatus = src.AddressStatus, }; // MPARTYACCOUNTOU mapping mirrors legacy GB4 PartyBLL.NewPartySaveTuned's bulk-OU block: // AgentId/RouteId/MRPPartyTypeId are always hardcoded -1; OUId/TaxTypeId come from the // BulkPostPartyOUDTO override (TMPOuId/TMPTaxTypeId); everything else is copied from the branch. // AccountId's legacy source was an Account record created elsewhere in the same save that this // refactor doesn't yet produce, so it's set to -1 (no linked account) pending that piece. // The (AccountId, OUId, PartyId, PartyBranchId) tuple is unique per MPARTYACCOUNTOU // (UKMPARTYACCOUNTOU_ACCOUNTIDOUIDPARTYIDPARTYBRANCHID) — re-saving the same party/branch/OU // combo must UPDATE the existing row rather than re-insert, same as legacy's update path // (PartyBLL.cs:1255-1314) carries forward CreditLimit/DebitLimit/CreditAllowed/DebitAllowed. private async Task SavePartyOUAsync( PartyDTO dto, AccountsDAL.DTO.PartyBranch.PartyBranchDTO branch, BulkPostPartyOUDTO ou, LoginDTO loginDTO, DbTransaction tx) { const int accountId = -1; var existing = await _partyOUDAL .GetPartyAccountOU(accountId, ou.TMPOuId, dto.PartyId, branch.PartyBranchId, loginDTO, tx) .ConfigureAwait(false); var partyOU = new PartyOUDTO { PartyId = dto.PartyId, PartyBranchId = branch.PartyBranchId, OUId = ou.TMPOuId, TaxTypeId = ou.TMPTaxTypeId, AccountId = accountId, AgentId = -1, RouteId = -1, MRPPartyTypeId = -1, PartyOUType = 0, InchargeId = branch.InchargeId, PartyOUCreditAllowed = existing != null ? (byte)existing.TempCreditAllowed : (byte)1, PartyOUDebitAllowed = existing != null ? (byte)existing.TempDebitAllowed : (byte)1, PartyOUCreditLimit = existing != null ? (double)existing.TempCreditLimit : 0, PartyOUDebitLimit = existing != null ? (double)existing.TempDebitLimit : 0, PartyOUIsPurchaseApplicable = branch.PartyBranchIsPurchaseApplicable, PartyOUIsSalesApplicable = branch.PartyBranchIsSalesApplicable, PartyOUSequenceNumber = branch.PartyBranchSequenceNumber, PartyOUStatus = 1, PurchasePaymentTermId = branch.PurchasePaymentTermId, PurchasePriceListId = branch.PurchasePriceListId, PurchaseTermsSetId = branch.PurchaseTermsSetId, SalesInchargeId = branch.SalesInchargeId, SalesPaymentTermId = branch.SalesPaymentTermId, SalesPriceListId = branch.SalesPriceListId, SalesTermsSetId = branch.SalesTermsSetId, TransporterId = branch.TransporterId, PartyOURouteIds = branch.PartyBranchRouteIds, PartyOUSourceType = loginDTO.SourceType, }; if (existing == null) { var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.PARTYOU, loginDTO).ConfigureAwait(false); partyOU.PartyOUId = auto.StartNumber; partyOU.PartyOUVersion = 1; await _partyOUDAL.SavePartyOU(partyOU, loginDTO, tx).ConfigureAwait(false); } else { partyOU.PartyOUId = existing.PartyOUId; partyOU.PartyOUVersion = (short)(existing.PartyOUVersion + 1); await _partyOUDAL.UpdatePartyOU(partyOU, loginDTO, tx).ConfigureAwait(false); } } public async Task DeleteParty(int partyId, LoginDTO loginDTO) { var tx = await _queryExecutor.BeginTransactionAsync(loginDTO).ConfigureAwait(false); try { await _partyDAL.DeleteParty(partyId, loginDTO, tx).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx).ConfigureAwait(false); return "Deleted successfully."; } catch { await _queryExecutor.RollbackAsync(tx).ConfigureAwait(false); throw; } } } }