from collections import defaultdict
from copy import deepcopy
from dataclasses import asdict
from datetime import date, datetime
from typing import Any, List, Optional, Sequence, cast

from bson import ObjectId
from international_bank_account.core.enums import BankAccountHolderTypes
from international_payroll.modules.remittances.internal.beans import (
    AggregateLiabilityCalculationBean,
    CompanyLiabilityTypeBean,
    CompanyLiabilityTypeSet,
    CompanyLiabilityVersionBean,
    CompanyLiabilityVersionKey,
    DebitAndCreditAmount,
    DefaultFundingSourceRemittanceInstruction,
    LiabilityCalculationAmount,
    LocalBankAccountRemittanceInstruction,
    PaymentBankAccount,
    PaymentMemoBase,
    PaymentMemoCRAFediAdditionalInstruction,
    PaymentMemoDefault,
    PaymentMemoForRefund,
    PaymentMemoQCFediAdditionalInstruction,
    RemittanceBatchAttemptBean,
    RemittanceBatchPaymentReciept,
    RemittanceDebitConfig,
    RemittanceFFRequestBatch,
    RemittanceFFRequestBatchAssignmentBean,
    RemittanceFFRequestBatchStatus,
    RemittanceInstruction,
    RemittanceIntentBean,
    RemittanceIntentFFRequestBean,
    RemittanceIntentStatusBean,
    RemittanceLiabilitySummary,
    RemittanceOrderBean,
    RemittancePaymentActivity,
    RemittancePaymentActivityDisplayInfo,
    RemittancePaymentAttemptBean,
    RemittancePaymentReceipt,
    RemittancePaymentReceiptKey,
    RemittancePayrollObjectSearchKey,
    RemittancePayrollObjectSnapshot,
    RemittanceVersionBean,
    RemittanceVersionKey,
    RoleLiabilityCalculationBean,
)
from international_payroll.modules.remittances.internal.data_store_framework import (
    UniqueKeyModelDataStore,
    VersionModelDataStore,
)
from international_payroll.modules.remittances.internal.enums import (
    DoNotRemitReasons,
    IntentStatus,
    LiabilityDebitBlockingCondition,
    LiabilityPayrollEntryObjectTypes,
    LiabilityPayrollObjectTypes,
    PaymentMemoTypes,
    RemittanceLiabilitySummaryState,
    RemittancePaymentActivityStatus,
    RemittanceResolutionStrategy,
)
from international_payroll.modules.remittances.internal.exceptions import (
    IntentStatusNotFoundException,
    LiabilityVersionNotFoundException,
    RemittanceVersionNotFoundException,
)
from international_payroll.modules.remittances.internal.intent_status_utils import IntentStatusUtil
from international_payroll.modules.remittances.internal.models.liability_models import (
    IPCompanyLiabilityType,
    IPCompanyLiabilityTypeSet,
    IPRemittancePayrollObjectSnapshot,
)
from international_payroll.modules.remittances.internal.models.payment_flow_models import (
    IPPaymentBankAccount,
)
from international_payroll.modules.remittances.internal.models.remittance_models import (
    BasePayrollEntryObject,
    DebitAndCreditDocument,
    IPAggregateLevelLiabilityCalculation,
    IPCompanyLiabilityVersion,
    IPPaymentMemoBase,
    IPPaymentMemoCRAFediAdditionalInstruction,
    IPPaymentMemoDefault,
    IPPaymentMemoForRefund,
    IPPaymentMemoQCFediAdditionalInstruction,
    IPRemittanceBatchAttempt,
    IPRemittanceBatchPaymentReciept,
    IPRemittanceDebitConfig,
    IPRemittanceFFRequestBatch,
    IPRemittanceFFRequestBatchAssignment,
    IPRemittanceFFRequestBatchStatus,
    IPRemittanceFulfillmentRequest,
    IPRemittanceIntent,
    IPRemittanceIntentStatus,
    IPRemittanceLiabilitySummary,
    IPRemittanceLocationBase,
    IPRemittanceLocationDefaultFundingSource,
    IPRemittanceLocationPaymentBankAccount,
    IPRemittanceOrder,
    IPRemittancePaymentActivity,
    IPRemittancePaymentActivityDisplayInfoDocument,
    IPRemittancePaymentAttempt,
    IPRemittancePaymentReciept,
    IPRemittanceVersion,
    IPRoleLevelLiabilityCalculation,
    LiabilityCalculationDocument,
    PrePayrollObject,
    ReconPrePayrollObject,
)
from international_payroll.modules.remittances.internal.types import BatchGroupKey
from international_payroll_beans.beans import CompanyPayrollEntity
from mongoengine import Q


def _cleanDataBeanDict(data: dict) -> dict:
    # todo: nuke this function https://rippling.atlassian.net/browse/GP-9857
    if "id" in data:
        data.pop("id")
    idKeys = [k for k in data.keys() if k[-2:] == "Id"]
    for k in idKeys:
        data[k[:-2]] = data.pop(k)
    return data


def _getPaymentBankAccountBean(ipPaymentBankAccount: IPPaymentBankAccount) -> PaymentBankAccount:
    return PaymentBankAccount(
        id=str(ipPaymentBankAccount.id),
        version=ipPaymentBankAccount.version,
        companyId=str(ipPaymentBankAccount.company.id),
        companyPayrollEntityId=str(ipPaymentBankAccount.companyPayrollEntity.id),
        changeEventId=str(ipPaymentBankAccount.changeEvent.id),
        purpose=ipPaymentBankAccount.purpose,
        bankAccountId=str(ipPaymentBankAccount.bankAccount.id),  # type: ignore[union-attr]
        paymentMethod=ipPaymentBankAccount.paymentMethod,
    )


def createPaymentBankAccount(bean: PaymentBankAccount) -> PaymentBankAccount:
    # TODO: nuke this function https://rippling.atlassian.net/browse/GP-9857
    data = asdict(bean)
    data = _cleanDataBeanDict(data)
    latestVersion: IPPaymentBankAccount = IPPaymentBankAccount.get_or_create(**data)
    return _getPaymentBankAccountBean(latestVersion)


def getLatestBankAccountForPurpose(
    companyId: str, companyPayrollEntityId: str, purpose: str
) -> Optional[PaymentBankAccount]:
    latestVersion: Optional[IPPaymentBankAccount] = (
        IPPaymentBankAccount.objects(company=companyId, companyPayrollEntity=companyPayrollEntityId, purpose=purpose)
        .order_by("-version")
        .first()
    )
    if latestVersion:
        return _getPaymentBankAccountBean(latestVersion)
    return None


def _getBatchPaymentRecieptDataBean(reciept: IPRemittanceBatchPaymentReciept) -> RemittanceBatchPaymentReciept:
    return RemittanceBatchPaymentReciept(
        id=str(reciept.id), batchAttemptId=str(reciept.batchPaymentAttempt.id), paymentOrderId=reciept.paymentOrderId
    )


class _RemittancePaymentReceiptDataStore(
    UniqueKeyModelDataStore[IPRemittancePaymentReciept, RemittancePaymentReceipt, RemittancePaymentReceiptKey]
):
    def convertBeanToMongo(self, bean: RemittancePaymentReceipt) -> IPRemittancePaymentReciept:
        return IPRemittancePaymentReciept(
            company=bean.companyId, paymentAttempt=bean.paymentAttemptId, paymentOrderId=bean.paymentOrderId
        )

    def convertMongoToBean(self, mongoDocument: IPRemittancePaymentReciept) -> RemittancePaymentReceipt:
        return RemittancePaymentReceipt(
            id=str(mongoDocument.id),
            companyId=str(mongoDocument.company.id),
            paymentAttemptId=str(mongoDocument.paymentAttempt.id),
            paymentOrderId=mongoDocument.paymentOrderId,
        )

    def getMongoQueryFromKey(self, key: RemittancePaymentReceiptKey) -> Q:
        return Q(company=key.companyId, paymentAttempt=key.paymentAttemptId)

    def getKeyFromBean(self, bean: RemittancePaymentReceipt) -> RemittancePaymentReceiptKey:
        return RemittancePaymentReceiptKey(companyId=bean.companyId, paymentAttemptId=bean.paymentAttemptId)


RemittancePaymentReceiptDataStore = _RemittancePaymentReceiptDataStore(
    IPRemittancePaymentReciept, RemittancePaymentReceipt, Exception
)


def getFFRequestsForBatch(batchId: str) -> Sequence[RemittanceIntentFFRequestBean]:
    batchAssignments = IPRemittanceFFRequestBatchAssignment.objects.filter(batch=str(batchId))
    return [_getRemittanceFulfillmentRequestBean(assignment.ffRequest) for assignment in batchAssignments]


def getOrNonePaymentAttempt(paymentAttemptId: str) -> Optional[RemittancePaymentAttemptBean]:
    attempt = IPRemittancePaymentAttempt.get_or_none(id=paymentAttemptId)
    if attempt is None:
        return None
    return _getRemittanceAttemptBean(attempt)


def createBatchAttempt(batchAttempt: RemittanceBatchAttemptBean) -> RemittanceBatchAttemptBean:
    return _getBatchAttempt(
        IPRemittanceBatchAttempt.objects.create(
            batch=batchAttempt.batch.id,
            clientId=batchAttempt.clientId,
            sourceBankAccountId=batchAttempt.clientId,
            sourceAccountHolderType=batchAttempt.sourceAccountHolderType,
            destBankAccountId=batchAttempt.destBankAccountId,
            destAccountHolderType=batchAttempt.destAccountHolderType,
            amount=batchAttempt.amount,
            currencyCode=batchAttempt.currencyCode,
            checkDate=batchAttempt.checkDate,
            moneyFlowExternalId=batchAttempt.moneyFlowExternalId,
        )
    )


def createBatchPaymentReciept(
    batchAttempt: RemittanceBatchAttemptBean, paymentOrderId: str
) -> RemittanceBatchPaymentReciept:
    recieptObject = IPRemittanceBatchPaymentReciept.objects.create(
        batchPaymentAttempt=batchAttempt.id, paymentOrderId=paymentOrderId
    )
    return _getBatchPaymentRecieptDataBean(recieptObject)


def getOrNoneRemittanceIntentForId(intentId: str) -> Optional[RemittanceIntentBean]:
    intent = IPRemittanceIntent.get_or_none(id=intentId)
    if intent is None:
        return None
    return _getRemittanceIntentBean(intent)


def getRemittanceIntentsForIds(intentIds: Sequence[str]) -> Sequence[RemittanceIntentBean]:
    intents = IPRemittanceIntent.objects.filter(id__in=intentIds)
    return [_getRemittanceIntentBean(intent) for intent in intents]


def getAllPaymentAttemptsForIntent(intent: RemittanceIntentBean) -> Sequence[RemittancePaymentAttemptBean]:
    paymentAttempts = IPRemittancePaymentAttempt.objects.filter(remittanceIntent=intent.id)
    return [_getRemittanceAttemptBean(attempt) for attempt in paymentAttempts]


def getLatestPaymentAttemptForIntent(intent: RemittanceIntentBean) -> Optional[RemittancePaymentAttemptBean]:
    paymentAttempt = IPRemittancePaymentAttempt.objects.filter(remittanceIntent=intent.id).order_by("-version").first()
    if paymentAttempt is None:
        return None
    return _getRemittanceAttemptBean(paymentAttempt)


def createPaymentAttempt(attempt: RemittancePaymentAttemptBean) -> RemittancePaymentAttemptBean:
    attemptObj = IPRemittancePaymentAttempt.objects.create(
        company=attempt.companyId,
        remittanceIntent=attempt.remittanceIntent.id,
        sourceRemittanceLocation=_makeRemittanceLocationIPObject(attempt.sourceRemittanceLocation),
        destRemittanceLocation=_makeRemittanceLocationIPObject(attempt.destRemittanceLocation),
        paymentMemo=_makePaymentMemoIPObject(attempt.paymentMemo),
        version=attempt.version,
    )

    return _getRemittanceAttemptBean(attemptObj)


def _makeRemittanceLocationIPObject(remittanceLocation: RemittanceInstruction) -> IPRemittanceLocationBase:
    if remittanceLocation.locationType == RemittanceResolutionStrategy.DEFAULT_FUNDING_SRC:
        remittanceLocation = cast(DefaultFundingSourceRemittanceInstruction, remittanceLocation)
        return IPRemittanceLocationDefaultFundingSource(
            locationType=remittanceLocation.locationType, bankAccountId=remittanceLocation.bankAccountId
        )

    elif remittanceLocation.locationType == RemittanceResolutionStrategy.GLOBAL_PAYMENT_ACCOUNT:
        remittanceLocation = cast(LocalBankAccountRemittanceInstruction, remittanceLocation)
        return IPRemittanceLocationPaymentBankAccount(
            locationType=remittanceLocation.locationType,
            bankAccountId=remittanceLocation.bankAccountId,
            accountHolderType=remittanceLocation.accountHolderType,
        )

    else:
        raise Exception(f"Unrecognized remittance location {remittanceLocation.locationType}")


def _getBatchAttempt(batchAttempt: IPRemittanceBatchAttempt) -> RemittanceBatchAttemptBean:
    return RemittanceBatchAttemptBean(
        id=batchAttempt.id,
        batch=batchAttempt.batch,
        clientId=batchAttempt.clientId,
        sourceBankAccountId=batchAttempt.sourceBankAccountId,
        sourceAccountHolderType=batchAttempt.sourceAccountHolderType,
        destBankAccountId=batchAttempt.destBankAccountId,
        destAccountHolderType=batchAttempt.destAccountHolderType,
        amount=batchAttempt.amount,
        currencyCode=batchAttempt.currencyCode,
        checkDate=batchAttempt.checkDate,
        moneyFlowExternalId=batchAttempt.moneyFlowExternalId,
    )


def _getRemittanceLocationBean(ipRemittanceLocationObject: IPRemittanceLocationBase) -> RemittanceInstruction:
    if ipRemittanceLocationObject.locationType == RemittanceResolutionStrategy.DEFAULT_FUNDING_SRC:
        return DefaultFundingSourceRemittanceInstruction(
            locationType=ipRemittanceLocationObject.locationType,
            bankAccountId=ipRemittanceLocationObject.bankAccountId,  # type: ignore[attr-defined]
            accountHolderType=BankAccountHolderTypes.company,  # todo: can probably store this on the IP object
        )

    elif ipRemittanceLocationObject.locationType == RemittanceResolutionStrategy.GLOBAL_PAYMENT_ACCOUNT:
        return LocalBankAccountRemittanceInstruction(
            locationType=ipRemittanceLocationObject.locationType,
            bankAccountId=ipRemittanceLocationObject.bankAccountId,  # type: ignore[attr-defined]
            accountHolderType=ipRemittanceLocationObject.accountHolderType,  # type: ignore[attr-defined]
        )

    else:
        raise Exception(f"Unrecognized remittance location {ipRemittanceLocationObject.locationType}")


def _makePaymentMemoIPObject(paymentMemo: PaymentMemoBase) -> IPPaymentMemoBase:
    if paymentMemo.paymentMemoType == PaymentMemoTypes.CRA_FEDI_INSTRUCTION:
        paymentMemo = cast(PaymentMemoCRAFediAdditionalInstruction, paymentMemo)
        return IPPaymentMemoCRAFediAdditionalInstruction(
            paymentMemoType=paymentMemo.paymentMemoType,
            text=paymentMemo.text,
            legalName=paymentMemo.legalName,
            craAccountNumber=paymentMemo.craAccountNumber,
            amountToBePaidInSmallestUnit=paymentMemo.amountToBePaidInSmallestUnit,
            grossPayrollInSmallestUnit=paymentMemo.grossPayrollInSmallestUnit,
            numEmployeesInvolved=paymentMemo.numEmployeesInvolved,
            remittancePeriodDate=paymentMemo.remittanceReason,
            remittanceReason=paymentMemo.remittanceReason,
        )
    elif paymentMemo.paymentMemoType == PaymentMemoTypes.QC_FEDI_INSTRUCTION:
        paymentMemo = cast(PaymentMemoQCFediAdditionalInstruction, paymentMemo)
        return IPPaymentMemoQCFediAdditionalInstruction(
            paymentMemoType=paymentMemo.paymentMemoType,
            text=paymentMemo.text,
            partnerName=paymentMemo.partnerName,
            rqPartnerId=paymentMemo.rqPartnerId,
            paymentPeriod=paymentMemo.paymentPeriod,
            depositDate=paymentMemo.depositDate,
            employerId=paymentMemo.employerId,
            employerFileNumber=paymentMemo.employerFileNumber,
            incomeTaxAmountInSmallestUnit=paymentMemo.incomeTaxAmountInSmallestUnit,
            qppAmountInSmallestUnit=paymentMemo.qppAmountInSmallestUnit,
            qpippAmountInSmallestUnit=paymentMemo.qpippAmountInSmallestUnit,
            hsfAmountInSmallestUnit=paymentMemo.hsfAmountInSmallestUnit,
            cnesstAmountInSmallestUnit=paymentMemo.cnesstAmountInSmallestUnit,
            remittancePaymentType=paymentMemo.remittancePaymentType,
        )
    elif paymentMemo.paymentMemoType == PaymentMemoTypes.DEFAULT:
        return IPPaymentMemoDefault(paymentMemoType=paymentMemo.paymentMemoType, text=paymentMemo.text)
    elif paymentMemo.paymentMemoType == PaymentMemoTypes.REFUND:
        return IPPaymentMemoForRefund(paymentMemoType=paymentMemo.paymentMemoType, text=paymentMemo.text)
    else:
        raise NotImplementedError(f"{paymentMemo.paymentMemoType} is not supported")


def _getPaymentMemoBean(ipPaymentMemoObject: IPPaymentMemoBase) -> PaymentMemoBase:
    if ipPaymentMemoObject.paymentMemoType == PaymentMemoTypes.CRA_FEDI_INSTRUCTION:
        ipPaymentMemoObject = cast(IPPaymentMemoCRAFediAdditionalInstruction, ipPaymentMemoObject)
        return PaymentMemoCRAFediAdditionalInstruction(
            paymentMemoType=ipPaymentMemoObject.paymentMemoType,
            text=ipPaymentMemoObject.text,
            legalName=ipPaymentMemoObject.legalName,
            craAccountNumber=ipPaymentMemoObject.craAccountNumber,
            amountToBePaidInSmallestUnit=ipPaymentMemoObject.amountToBePaidInSmallestUnit,
            grossPayrollInSmallestUnit=ipPaymentMemoObject.grossPayrollInSmallestUnit,
            numEmployeesInvolved=ipPaymentMemoObject.numEmployeesInvolved,
            remittancePeriodDate=ipPaymentMemoObject.remittancePeriodDate,
            remittanceReason=ipPaymentMemoObject.remittanceReason,
        )
    elif ipPaymentMemoObject.paymentMemoType == PaymentMemoTypes.QC_FEDI_INSTRUCTION:
        ipPaymentMemoObject = cast(IPPaymentMemoQCFediAdditionalInstruction, ipPaymentMemoObject)
        return PaymentMemoQCFediAdditionalInstruction(
            paymentMemoType=ipPaymentMemoObject.paymentMemoType,
            text=ipPaymentMemoObject.text,
            partnerName=ipPaymentMemoObject.partnerName,
            rqPartnerId=ipPaymentMemoObject.rqPartnerId,
            paymentPeriod=ipPaymentMemoObject.paymentPeriod,
            depositDate=ipPaymentMemoObject.depositDate,
            employerId=ipPaymentMemoObject.employerId,
            employerFileNumber=ipPaymentMemoObject.employerFileNumber,
            incomeTaxAmountInSmallestUnit=ipPaymentMemoObject.incomeTaxAmountInSmallestUnit,
            qppAmountInSmallestUnit=ipPaymentMemoObject.qppAmountInSmallestUnit,
            qpippAmountInSmallestUnit=ipPaymentMemoObject.qpippAmountInSmallestUnit,
            hsfAmountInSmallestUnit=ipPaymentMemoObject.hsfAmountInSmallestUnit,
            cnesstAmountInSmallestUnit=ipPaymentMemoObject.cnesstAmountInSmallestUnit,
            remittancePaymentType=ipPaymentMemoObject.remittancePaymentType,
        )
    elif ipPaymentMemoObject.paymentMemoType == PaymentMemoTypes.DEFAULT:
        return PaymentMemoDefault(paymentMemoType=ipPaymentMemoObject.paymentMemoType, text=ipPaymentMemoObject.text)
    elif ipPaymentMemoObject.paymentMemoType == PaymentMemoTypes.REFUND:
        return PaymentMemoForRefund(paymentMemoType=ipPaymentMemoObject.paymentMemoType, text=ipPaymentMemoObject.text)
    else:
        raise NotImplementedError(f"{ipPaymentMemoObject.paymentMemoType} is not supported")


def _getRemittanceAttemptBean(attempt: IPRemittancePaymentAttempt) -> RemittancePaymentAttemptBean:
    return RemittancePaymentAttemptBean(
        id=str(attempt.id),
        companyId=str(attempt.company.id),
        remittanceIntent=_getRemittanceIntentBean(attempt.remittanceIntent),
        sourceRemittanceLocation=_getRemittanceLocationBean(attempt.sourceRemittanceLocation),
        destRemittanceLocation=_getRemittanceLocationBean(attempt.destRemittanceLocation),
        paymentMemo=_getPaymentMemoBean(attempt.paymentMemo),
        version=attempt.version,
    )


def _getIntentStatus(intentObject: IPRemittanceIntentStatus) -> RemittanceIntentStatusBean:
    return RemittanceIntentStatusBean(
        id=str(intentObject.id),
        companyId=str(intentObject.company.id),
        remittanceIntentId=str(intentObject.remittanceIntent.id),
        status=intentObject.status,
        counter=intentObject.counter,
        reason=intentObject.reason,
        createdAt=intentObject.createdAt,
    )


def _getRemittanceDebitConfigBean(ipRemittanceDebitConfig: IPRemittanceDebitConfig) -> RemittanceDebitConfig:
    return RemittanceDebitConfig(
        id=str(ipRemittanceDebitConfig.id),
        companyId=ipRemittanceDebitConfig.companyId,
        companyPayrollEntityId=ipRemittanceDebitConfig.companyPayrollEntityId,
        countryCode=ipRemittanceDebitConfig.countryCode,
        version=ipRemittanceDebitConfig.version,
        payrollObjectId=ipRemittanceDebitConfig.payrollObjectId,
        payrollObjectType=ipRemittanceDebitConfig.payrollObjectType,
        doNotDebitTaxCodes=ipRemittanceDebitConfig.doNotDebitTaxCodes,
        doNotDebitLegalDeductionCodes=ipRemittanceDebitConfig.doNotDebitLegalDeductionCodes,
        createdAt=ipRemittanceDebitConfig.createdAt,
    )


def writeIntentStatus(
    intent: RemittanceIntentBean, newStatus: IntentStatus, counter: int, reason: str = ""
) -> RemittanceIntentStatusBean:
    intentObject = IPRemittanceIntentStatus.objects.create(
        company=intent.companyId, remittanceIntent=intent.id, status=newStatus, counter=counter, reason=reason
    )
    intentStatusBean = _getIntentStatus(intentObject)
    RemittanceLiabilitySummaryDataStore.onRemittanceIntentStatusCreated(intentStatusBean)
    return intentStatusBean


def getAllIntentIds() -> Sequence[str]:
    return [str(obj["_id"]) for obj in IPRemittanceIntent.objects_across_company.only("id").as_pymongo()]


def getLatestIntentStatus(intent: RemittanceIntentBean) -> RemittanceIntentStatusBean:
    intentStatusObj = (
        IPRemittanceIntentStatus.objects.filter(company=intent.companyId, remittanceIntent=intent.id)
        .order_by("-counter")
        .first()
    )
    if not intentStatusObj:
        # always require a status
        raise IntentStatusNotFoundException(f"Missing intent status for intent.id {intent.id}")
    return _getIntentStatus(intentStatusObj)


def getLatestIntentStatusesForIntentIds(
    companyId: str, intentIds: Sequence[str]
) -> Sequence[RemittanceIntentStatusBean]:
    pipeline = [
        {
            "$match": {
                "company": ObjectId(companyId),
                "remittanceIntent": {"$in": [ObjectId(objectId) for objectId in intentIds]},
            },
        },
        {"$sort": {"counter": -1}},
        {
            "$group": {
                "_id": {"remittanceIntent": "$remittanceIntent"},
                "latest": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latest"}},
    ]
    intentStatusObjs = IPRemittanceIntentStatus.objects.aggregate(*pipeline)
    return [
        RemittanceIntentStatusBean(
            id=str(data["_id"]),
            companyId=str(data["company"]),
            remittanceIntentId=str(data["remittanceIntent"]),
            status=str(data["status"]),  # type: ignore[arg-type]
            counter=int(data["counter"]),
            reason=str(data["reason"]) if "reason" in data else None,
            createdAt=data["createdAt"],
        )
        for data in intentStatusObjs
    ]


def getRemittanceIntentsForRemittanceOrders(
    companyId: str, remittanceOrders: Sequence[RemittanceOrderBean]
) -> Sequence[RemittanceIntentBean]:
    intentObjs = IPRemittanceIntent.objects.filter(
        company=companyId, remittanceOrder__in=[order.id for order in remittanceOrders]
    )
    return [_getRemittanceIntentBean(intentObj) for intentObj in intentObjs]


def getRemittanceIntentsForRemittanceOrder(remittanceOrder: RemittanceOrderBean) -> Sequence[RemittanceIntentBean]:
    intentObjs = IPRemittanceIntent.objects.filter(remittanceOrder=remittanceOrder.id)
    return [_getRemittanceIntentBean(intentObj) for intentObj in intentObjs]


def hasRemittanceIntentsForOrderId(remittanceOrderId: str) -> bool:
    return IPRemittanceIntent.objects(remittanceOrder=remittanceOrderId).exists()


def createRemittanceIntentFFRequest(intent: RemittanceIntentBean) -> RemittanceIntentFFRequestBean:
    ffRequest = getLatestFFRequestForIntent(intent)
    if ffRequest is None:
        version = 0
    else:
        version = ffRequest.version + 1
    return _getRemittanceFulfillmentRequestBean(
        IPRemittanceFulfillmentRequest.objects.create(intent=intent.id, version=version)
    )


def createRemittanceIntent(
    intentBean: RemittanceIntentBean, deps: Sequence[RemittanceIntentBean]
) -> RemittanceIntentBean:
    return _getRemittanceIntentBean(
        IPRemittanceIntent.objects.create(
            company=intentBean.companyId,
            remittanceOrder=intentBean.remittanceOrder.id,
            amount=intentBean.amount,
            currencyCode=intentBean.currencyCode,
            sourceInstruction=intentBean.sourceInstruction,
            destInstruction=intentBean.destInstruction,
            deps=deps,
        )
    )


class _CompanyLiabilityVersionDataStore(
    VersionModelDataStore[IPCompanyLiabilityVersion, CompanyLiabilityVersionBean, CompanyLiabilityVersionKey]
):
    def getMongoQueryFromKey(self, key: CompanyLiabilityVersionKey) -> Q:
        return Q(company=key.companyId, payrollObjectId=key.payrollObjectId, payrollObjectType=key.payrollObjectType)

    def convertMongoToBean(self, mongoDocument: IPCompanyLiabilityVersion) -> CompanyLiabilityVersionBean:
        return CompanyLiabilityVersionBean(
            id=str(mongoDocument.id),
            companyId=str(mongoDocument.company.id),
            countryCode=mongoDocument.countryCode,
            reasonCode=mongoDocument.reasonCode,
            payrollObjectId=mongoDocument.payrollObjectId,
            payrollObjectType=mongoDocument.payrollObjectType,
            liabilitySet=_getCompanyLiabilityTypeSet(mongoDocument.liabilitySet),
            companyPayrollEntityId=str(mongoDocument.companyPayrollEntity.id),
            checkDate=mongoDocument.checkDate,
        )

    def convertBeanToMongo(self, bean: CompanyLiabilityVersionBean) -> IPCompanyLiabilityVersion:
        return IPCompanyLiabilityVersion(
            company=bean.companyId,
            payrollObjectId=bean.payrollObjectId,
            payrollObjectType=bean.payrollObjectType,
            countryCode=bean.countryCode,
            reasonCode=bean.reasonCode,
            liabilitySet=bean.liabilitySet.id,
            companyPayrollEntity=bean.companyPayrollEntityId,
            checkDate=bean.checkDate,
        )

    def getKeyFromBean(self, bean: CompanyLiabilityVersionBean) -> CompanyLiabilityVersionKey:
        return CompanyLiabilityVersionKey(
            companyId=bean.companyId, payrollObjectId=bean.payrollObjectId, payrollObjectType=bean.payrollObjectType
        )


CompanyLiabilityVersionDataStore = _CompanyLiabilityVersionDataStore(
    IPCompanyLiabilityVersion, CompanyLiabilityVersionBean, LiabilityVersionNotFoundException
)


class _RemittanceVersionDataStore(
    VersionModelDataStore[IPRemittanceVersion, RemittanceVersionBean, RemittanceVersionKey]
):
    def getMongoQueryFromKey(self, key: RemittanceVersionKey) -> Q:
        return Q(
            company=key.companyId,
            payrollObjectId=key.payrollObjectId,
            payrollObjectType=key.payrollObjectType,
            liabilityCode=key.liabilityCode,
        )

    def convertMongoToBean(self, mongoDocument: IPRemittanceVersion) -> RemittanceVersionBean:
        return RemittanceVersionBean(
            id=str(mongoDocument.id),
            companyId=str(mongoDocument.company.id),
            payrollObjectId=mongoDocument.payrollObjectId,
            payrollObjectType=mongoDocument.payrollObjectType,
            reasonCode=mongoDocument.reasonCode,
            dueDate=mongoDocument.dueDate,
            fulfillAfterDate=mongoDocument.fulfillAfterDate,
            liabilityCode=mongoDocument.liabilityCode,
            liabilityVersionId=str(mongoDocument.liabilityVersion.id),
            doNotRemit=mongoDocument.doNotRemit,
        )

    def convertBeanToMongo(self, bean: RemittanceVersionBean) -> IPRemittanceVersion:
        return IPRemittanceVersion(
            company=bean.companyId,
            reasonCode=bean.reasonCode,
            payrollObjectId=bean.payrollObjectId,
            payrollObjectType=bean.payrollObjectType,
            liabilityCode=bean.liabilityCode,
            dueDate=bean.dueDate,
            fulfillAfterDate=bean.fulfillAfterDate,
            liabilityVersion=bean.liabilityVersionId,
            doNotRemit=bean.doNotRemit,
        )

    def getKeyFromBean(self, bean: RemittanceVersionBean) -> RemittanceVersionKey:
        return RemittanceVersionKey(
            companyId=bean.companyId,
            payrollObjectId=bean.payrollObjectId,
            payrollObjectType=bean.payrollObjectType,
            liabilityCode=bean.liabilityCode,
        )


RemittanceVersionDataStore = _RemittanceVersionDataStore(
    IPRemittanceVersion, RemittanceVersionBean, RemittanceVersionNotFoundException
)


def getLatestRemittanceVersionIdsInBulk(keys: Sequence[RemittancePayrollObjectSearchKey]) -> Sequence[str]:
    if not keys:
        return []

    conditions = [{"payrollObjectId": key.payrollObjectId, "payrollObjectType": key.payrollObjectType} for key in keys]

    pipeline = [
        {"$match": {"$or": conditions}},
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {
                    "payrollObjectId": "$payrollObjectId",
                    "payrollObjectType": "$payrollObjectType",
                    "liabilityCode": "$liabilityCode",
                },
                "latest": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latest"}},
    ]
    aggregationResults = list(IPRemittanceVersion.objects_across_company.aggregate(*pipeline))
    return [str(remittanceVersion["_id"]) for remittanceVersion in aggregationResults]


def getLatestFFRequestForIntent(intent: RemittanceIntentBean) -> Optional[RemittanceIntentFFRequestBean]:
    ffRequest = IPRemittanceFulfillmentRequest.objects.filter(intent=intent.id).order_by("-version").first()
    if ffRequest is None:
        return None
    return _getRemittanceFulfillmentRequestBean(ffRequest)


def getLatestRemittanceVersionsForLiabilityCode(
    companyId: str,
    liabilityCode: str,
) -> Sequence[RemittanceVersionBean]:
    pipeline = [
        {"$match": {"company": ObjectId(companyId), "liabilityCode": liabilityCode}},
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {
                    "payrollObjectId": "$payrollObjectId",
                    "payrollObjectType": "$payrollObjectType",
                    "liabilityCode": "$liabilityCode",
                },
                "latest": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latest"}},
    ]
    aggregationResults = list(IPRemittanceVersion.objects.aggregate(*pipeline))
    return [getRemittanceVersionBeanFromPyMongo(remittanceVersion) for remittanceVersion in aggregationResults]


def getLatestRemittanceVersions(
    companyId: str,
    objectId: str,
    objectType: LiabilityPayrollObjectTypes,
) -> Sequence[RemittanceVersionBean]:
    pipeline = [
        {"$match": {"company": ObjectId(companyId), "payrollObjectId": objectId, "payrollObjectType": objectType}},
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {"payrollObjectId": "$payrollObjectId", "liabilityCode": "$liabilityCode"},
                "latestRemittance": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latestRemittance"}},
    ]
    aggregationResults = list(IPRemittanceVersion.objects.aggregate(*pipeline))
    return [getRemittanceVersionBeanFromPyMongo(remittanceVersion) for remittanceVersion in aggregationResults]


def getRemittanceVersionBeanFromPyMongo(remittanceVersion: dict[str, Any]) -> RemittanceVersionBean:
    return RemittanceVersionBean(
        id=str(remittanceVersion["_id"]),
        companyId=str(remittanceVersion["company"]),
        liabilityCode=str(remittanceVersion["liabilityCode"]),
        payrollObjectId=str(remittanceVersion["payrollObjectId"]),
        payrollObjectType=cast(LiabilityPayrollObjectTypes, str(remittanceVersion["payrollObjectType"])),
        reasonCode=str(remittanceVersion["reasonCode"]),
        dueDate=remittanceVersion["dueDate"],
        fulfillAfterDate=remittanceVersion["fulfillAfterDate"],
        liabilityVersionId=str(remittanceVersion["liabilityVersion"]),
        doNotRemit=remittanceVersion["doNotRemit"],
    )


def createRoleLiabilityCalculation(
    liabilityVersion: CompanyLiabilityVersionBean,
    liabilityCalculation: RoleLiabilityCalculationBean,
) -> None:
    objectData = _getPayrollEntryObjectData(
        liabilityCalculation.payrollEntryObjectId, liabilityCalculation.payrollEntryObjectType
    )
    liabilityAmountDocument = _getLiabilityCalculationDocument(liabilityCalculation.liabilityAmount)
    _ = IPRoleLevelLiabilityCalculation.objects.create(
        company=liabilityCalculation.companyId,
        role=liabilityCalculation.roleId,
        liabilityVersion=liabilityVersion.id,
        liabilityCode=liabilityCalculation.liabilityCode,
        currencyCode=liabilityCalculation.currencyCode,
        liabilityAmount=liabilityAmountDocument,
        payrollEntryObjectType=liabilityCalculation.payrollEntryObjectType,
        payrollEntryObjectId=liabilityCalculation.payrollEntryObjectId,
        payrollEntryObjectData=objectData,
    )


def createAggregateLiabilityCalculation(
    liabilityVersion: CompanyLiabilityVersionBean,
    liabilityCalculation: AggregateLiabilityCalculationBean,
) -> None:
    liabilityAmountDocument = _getLiabilityCalculationDocument(liabilityCalculation.liabilityAmount)
    _ = IPAggregateLevelLiabilityCalculation.objects.create(
        company=liabilityCalculation.companyId,
        liabilityVersion=liabilityVersion.id,
        liabilityCode=liabilityCalculation.liabilityCode,
        currencyCode=liabilityCalculation.currencyCode,
        liabilityAmount=liabilityAmountDocument,
    )


def createRemittanceOrder(remittanceVersion: RemittanceVersionBean, remittanceOrder: RemittanceOrderBean) -> None:
    _ = IPRemittanceOrder.objects.create(
        company=remittanceOrder.companyId,
        remittanceVersion=remittanceVersion.id,
        currencyCode=remittanceOrder.currencyCode,
        companyPayrollEntity=remittanceOrder.companyPayrollEntityId,
        liabilityAmount=remittanceOrder.liabilityAmount,
        moneyPath=remittanceOrder.moneyPath,
    )


def copyRemittanceOrderToNewRemittanceVersion(
    companyId: str, oldRemittanceOrderId: str, newRemittanceVersionId: str
) -> None:
    oldOrder = IPRemittanceOrder.objects.get(id=oldRemittanceOrderId)
    if not oldOrder:
        raise Exception(f"Could not find an order for id {oldRemittanceOrderId}")
    if not IPRemittanceVersion.objects(id=newRemittanceVersionId).exists():
        raise Exception(f"The remittance version does not exist for id {newRemittanceVersionId}")

    newOrder = deepcopy(oldOrder)
    newOrder.id = None
    newOrder.remittanceVersion = newRemittanceVersionId
    newOrder.save()

    oldIntents = IPRemittanceIntent.objects(company=companyId, remittanceOrder=oldRemittanceOrderId).only("id")
    for oldIntent in oldIntents:
        copyRemittanceIntentsToNewRemittanceOrderId(companyId, str(oldIntent.id), str(newOrder.id))
    return


def copyRemittanceIntentsToNewRemittanceOrderId(companyId: str, oldIntentId: str, newOrderId: str) -> None:
    oldIntent = IPRemittanceIntent.objects.get(id=oldIntentId)
    if not IPRemittanceOrder.objects(id=newOrderId).exists():
        raise Exception(f"The remittance order does not exist {newOrderId}")

    newIntent = deepcopy(oldIntent)
    newIntent.id = None
    newIntent.remittanceOrder = newOrderId
    newIntent.save()

    oldStatuses = IPRemittanceIntentStatus.objects(company=companyId, remittanceIntent=oldIntentId)
    for oldStatus in oldStatuses:
        copyRemittanceIntentStatusToNewRemittanceIntent(companyId, str(oldStatus.id), str(newIntent.id))

    oldAttempts = IPRemittancePaymentAttempt.objects(company=companyId, remittanceIntent=oldIntentId)
    for oldAttempt in oldAttempts:
        copyRemittancePaymentAttemptToNewRemittanceIntent(companyId, str(oldAttempt.id), str(newIntent.id))
    return


def copyRemittanceIntentStatusToNewRemittanceIntent(companyId: str, oldIntentStatusId: str, newIntentId: str) -> None:
    oldIntentStatus = IPRemittanceIntentStatus.objects.get(id=oldIntentStatusId)
    if not IPRemittanceIntent.objects(id=newIntentId).exists():
        raise Exception(f"The remittance intent does not exist {newIntentId}")

    newIntentStatus = deepcopy(oldIntentStatus)
    newIntentStatus.id = None
    newIntentStatus.remittanceIntent = newIntentId
    newIntentStatus.save()


def copyRemittancePaymentAttemptToNewRemittanceIntent(companyId: str, oldAttemptId: str, newIntentId: str) -> None:
    oldAttempt = IPRemittancePaymentAttempt.objects.get(id=oldAttemptId)
    if not IPRemittanceIntent.objects(id=newIntentId).exists():
        raise Exception(f"The remittance intent does not exist {newIntentId}")

    newAttempt = deepcopy(oldAttempt)
    newAttempt.id = None
    newAttempt.remittanceIntent = newIntentId
    newAttempt.save()

    oldReceipt = RemittancePaymentReceiptDataStore.getOrNone(
        key=RemittancePaymentReceiptKey(companyId=companyId, paymentAttemptId=oldAttemptId)
    )
    if oldReceipt:
        newReceipt = deepcopy(oldReceipt)
        newReceipt.id = ""
        newReceipt.paymentAttemptId = str(newAttempt.id)
        RemittancePaymentReceiptDataStore.create(newReceipt)
    return


def getRoleLiabilityCalculationsForCompanyLiabilityVersion(
    liabilityVersion: CompanyLiabilityVersionBean,
) -> List[RoleLiabilityCalculationBean]:
    liabilityCalculations = IPRoleLevelLiabilityCalculation.objects.filter(
        company=liabilityVersion.companyId, liabilityVersion=liabilityVersion.id
    )
    return [_getRoleLiabilityCalculationBean(liabilityCalculation) for liabilityCalculation in liabilityCalculations]


def getAggregateLiabilityCalculationsForCompanyLiabilityVersion(
    liabilityVersion: CompanyLiabilityVersionBean,
) -> List[AggregateLiabilityCalculationBean]:
    liabilityCalculations = IPAggregateLevelLiabilityCalculation.objects.filter(
        company=liabilityVersion.companyId, liabilityVersion=liabilityVersion.id
    )
    return [
        _getAggregateLiabilityCalculationBean(liabilityCalculation) for liabilityCalculation in liabilityCalculations
    ]


def getRemittanceOrdersForRemittanceVersion(
    remittanceVersion: RemittanceVersionBean,
) -> List[RemittanceOrderBean]:
    remittanceOrderObjs = IPRemittanceOrder.objects.filter(remittanceVersion=remittanceVersion.id)
    return [
        _getRemittanceOrderBean(remittanceOrderObj, remittanceVersion) for remittanceOrderObj in remittanceOrderObjs
    ]


def getRemittanceOrdersForRemittanceVersions(
    companyId: str,
    remittanceVersions: Sequence[RemittanceVersionBean],
) -> List[RemittanceOrderBean]:
    versionIds = [version.id for version in remittanceVersions]
    remittanceOrderObjs = IPRemittanceOrder.objects.filter(company=companyId, remittanceVersion__in=versionIds)
    versionsById = {str(version.id): version for version in remittanceVersions}
    return [
        _getRemittanceOrderBean(remittanceOrderObj, versionsById[str(remittanceOrderObj.remittanceVersion.id)])
        for remittanceOrderObj in remittanceOrderObjs
    ]


def _getRemittanceFulfillmentRequestBean(
    ffRequestObj: IPRemittanceFulfillmentRequest,
) -> RemittanceIntentFFRequestBean:
    return RemittanceIntentFFRequestBean(
        id=str(ffRequestObj.id), intent=_getRemittanceIntentBean(ffRequestObj.intent), version=ffRequestObj.version
    )


def _getDebitAndCreditDocument(amount: DebitAndCreditAmount) -> DebitAndCreditDocument:
    return DebitAndCreditDocument(
        debitAmount=amount.debitAmount,
        creditAmount=amount.creditAmount,
        netAmount=amount.netAmount,
    )


def _getLiabilityCalculationDocument(
    liabilityCalculationAmount: LiabilityCalculationAmount,
) -> LiabilityCalculationDocument:
    return LiabilityCalculationDocument(
        totalAmount=liabilityCalculationAmount.totalAmount,
        legalDeductionCodeAmounts={
            code: _getDebitAndCreditDocument(amount)
            for code, amount in liabilityCalculationAmount.legalDeductionCodeAmounts.items()
        },
        taxCodeAmounts={
            code: _getDebitAndCreditDocument(amount)
            for code, amount in liabilityCalculationAmount.taxCodeAmounts.items()
        },
    )


def _getDebitAndCreditAmount(document: DebitAndCreditDocument) -> DebitAndCreditAmount:
    return DebitAndCreditAmount(
        debitAmount=document.debitAmount,
        creditAmount=document.creditAmount,
        netAmount=document.netAmount,
    )


def _getLiabilityCalculationAmount(
    liabilityCalculationDocument: LiabilityCalculationDocument,
) -> LiabilityCalculationAmount:
    return LiabilityCalculationAmount(
        totalAmount=liabilityCalculationDocument.totalAmount,
        legalDeductionCodeAmounts={
            code: _getDebitAndCreditAmount(amount)
            for code, amount in liabilityCalculationDocument.legalDeductionCodeAmounts.items()
        },
        taxCodeAmounts={
            code: _getDebitAndCreditAmount(amount)
            for code, amount in liabilityCalculationDocument.taxCodeAmounts.items()
        },
    )


def _getRoleLiabilityCalculationBean(
    liabilityCalculationObj: IPRoleLevelLiabilityCalculation,
) -> RoleLiabilityCalculationBean:
    liabilityAmount = _getLiabilityCalculationAmount(liabilityCalculationObj.liabilityAmount)
    return RoleLiabilityCalculationBean(
        companyId=str(liabilityCalculationObj.company.id),
        roleId=str(liabilityCalculationObj.role.id),
        liabilityCode=liabilityCalculationObj.liabilityCode,
        currencyCode=liabilityCalculationObj.currencyCode,
        liabilityAmount=liabilityAmount,
        liabilityVersionId=str(liabilityCalculationObj.liabilityVersion.id),
        payrollObjectId=liabilityCalculationObj.liabilityVersion.payrollObjectId,
        payrollObjectType=liabilityCalculationObj.liabilityVersion.payrollObjectType,
        payrollEntryObjectId=liabilityCalculationObj.payrollEntryObjectId,
        payrollEntryObjectType=liabilityCalculationObj.payrollEntryObjectType,
    )


def _getAggregateLiabilityCalculationBean(
    liabilityCalculationObj: IPAggregateLevelLiabilityCalculation,
) -> AggregateLiabilityCalculationBean:
    liabilityAmount = _getLiabilityCalculationAmount(liabilityCalculationObj.liabilityAmount)
    return AggregateLiabilityCalculationBean(
        id=str(liabilityCalculationObj.id),
        companyId=str(liabilityCalculationObj.company.id),
        liabilityCode=liabilityCalculationObj.liabilityCode,
        currencyCode=liabilityCalculationObj.currencyCode,
        liabilityAmount=liabilityAmount,
        liabilityVersionId=str(liabilityCalculationObj.liabilityVersion.id),
        payrollObjectId=liabilityCalculationObj.liabilityVersion.payrollObjectId,
        payrollObjectType=liabilityCalculationObj.liabilityVersion.payrollObjectType,
    )


def _getPayrollEntryObjectData(objectId: str, objectType: LiabilityPayrollEntryObjectTypes) -> BasePayrollEntryObject:
    if objectType == LiabilityPayrollEntryObjectTypes.PRE:
        return PrePayrollObject(preVersion=objectId)
    elif objectType == LiabilityPayrollEntryObjectTypes.RECON_PRE:
        return ReconPrePayrollObject(reconPreVersion=objectId)
    else:
        raise Exception(f"Unknown objectType {objectType}")


def _getRemittanceOrderBean(
    remittanceOrder: IPRemittanceOrder, remittanceVersion: RemittanceVersionBean
) -> RemittanceOrderBean:
    return RemittanceOrderBean(
        id=str(remittanceOrder.id),
        companyId=str(remittanceOrder.company.id),
        payrollObjectId=remittanceVersion.payrollObjectId,
        payrollObjectType=remittanceVersion.payrollObjectType,
        dueDate=remittanceVersion.dueDate,
        fulfillAfterDate=remittanceVersion.fulfillAfterDate,
        liabilityCode=remittanceVersion.liabilityCode,
        companyPayrollEntityId=str(remittanceOrder.companyPayrollEntity.id),
        currencyCode=remittanceOrder.currencyCode,
        liabilityAmount=remittanceOrder.liabilityAmount,
        moneyPath=list(remittanceOrder.moneyPath),
        remittanceVersionId=str(remittanceVersion.id),
    )


def createCompanyLiabilityTypeSet(
    companyId: str,
    companyPayrollEntityId: str,
    countryCode: str,
    effectiveDateTime: datetime,
    liabilityTypes: Sequence[CompanyLiabilityTypeBean],
) -> CompanyLiabilityTypeSet:
    ipCompanyLiabilityTypeSet = IPCompanyLiabilityTypeSet.get_or_create(
        company=companyId,
        companyPayrollEntity=companyPayrollEntityId,
        countryCode=countryCode,
        effectiveDateTime=effectiveDateTime,
    )

    liabilityTypeBeans = []
    for liabilityType in liabilityTypes:
        ipLiabilityType = IPCompanyLiabilityType.get_or_create(
            company=companyId,
            liabilityTypeSet=ipCompanyLiabilityTypeSet,
            doNotRemit=liabilityType.doNotRemit,
            doNotRemitReason=liabilityType.doNotRemitReason,
            currencyCode=liabilityType.currencyCode,
            liabilityCode=liabilityType.liabilityCode,
            legalDeductionCodes=liabilityType.legalDeductionCodes,
            taxCodes=liabilityType.taxCodes,
            debitBlockingConditions=[c for c in liabilityType.debitBlockingConditions],
        )
        liabilityTypeBeans.append(_getCompanyLiabilityTypeBean(ipLiabilityType))

    return CompanyLiabilityTypeSet(
        id=str(ipCompanyLiabilityTypeSet.id),
        countryCode=str(ipCompanyLiabilityTypeSet.countryCode),
        companyId=str(ipCompanyLiabilityTypeSet.company.id),
        companyPayrollEntityId=str(ipCompanyLiabilityTypeSet.companyPayrollEntity.id),
        effectiveDateTime=ipCompanyLiabilityTypeSet.effectiveDateTime,
        companyLiabilities=liabilityTypeBeans,
        isSystemDefault=ipCompanyLiabilityTypeSet.isSystemDefault,
    )


def getCompanyLiabilityTypeSetsById(liabilitySetIds: Sequence[str]) -> Sequence[CompanyLiabilityTypeSet]:
    ipCompanyLiabilityTypeSets = IPCompanyLiabilityTypeSet.objects.filter(id__in=liabilitySetIds)
    return [_getCompanyLiabilityTypeSet(liabilitySet) for liabilitySet in ipCompanyLiabilityTypeSets]


def _getCompanyLiabilityTypeSet(liabilitySet: IPCompanyLiabilityTypeSet) -> CompanyLiabilityTypeSet:
    liabilityTypes = IPCompanyLiabilityType.objects.filter(
        company=str(liabilitySet.company.id),
        liabilityTypeSet=liabilitySet.id,
    )

    liabilities: List[CompanyLiabilityTypeBean] = []
    for liabilityType in liabilityTypes:
        liabilities.append(_getCompanyLiabilityTypeBean(liabilityType))

    return CompanyLiabilityTypeSet(
        id=str(liabilitySet.id),
        countryCode=str(liabilitySet.countryCode),
        companyId=str(liabilitySet.company.id),
        companyPayrollEntityId=str(liabilitySet.companyPayrollEntity.id),
        effectiveDateTime=liabilitySet.effectiveDateTime,
        companyLiabilities=liabilities,
        isSystemDefault=liabilitySet.isSystemDefault,
    )


def getLiabilitySetOn(
    companyId: str, companyPayrollEntityId: str, effectiveDateTime: datetime
) -> Optional[CompanyLiabilityTypeSet]:
    for liabilitySet in IPCompanyLiabilityTypeSet.objects.filter(
        company=companyId, companyPayrollEntity=companyPayrollEntityId
    ).order_by("-effectiveDateTime"):
        if liabilitySet.effectiveDateTime <= effectiveDateTime:
            return _getCompanyLiabilityTypeSet(liabilitySet)

    return None


def getLiabilitySetForId(liabilitySetId: str) -> Optional[CompanyLiabilityTypeSet]:
    liabilitySet = IPCompanyLiabilityTypeSet.get_or_none(id=liabilitySetId)
    if liabilitySet is None:
        return None
    return _getCompanyLiabilityTypeSet(liabilitySet)


def _getCompanyLiabilityTypeBean(ipCompanyLiabilityType: IPCompanyLiabilityType) -> CompanyLiabilityTypeBean:
    return CompanyLiabilityTypeBean(
        doNotRemit=ipCompanyLiabilityType.doNotRemit,
        doNotRemitReason=cast(DoNotRemitReasons, ipCompanyLiabilityType.doNotRemitReason),
        liabilityCode=ipCompanyLiabilityType.liabilityCode,
        currencyCode=ipCompanyLiabilityType.currencyCode,
        legalDeductionCodes=ipCompanyLiabilityType.legalDeductionCodes,
        taxCodes=ipCompanyLiabilityType.taxCodes,
        isSystemDefault=ipCompanyLiabilityType.isSystemDefault,
        debitBlockingConditions=[
            LiabilityDebitBlockingCondition(c) for c in ipCompanyLiabilityType.debitBlockingConditions
        ],
    )


def _getRemittanceIntentBean(intent: IPRemittanceIntent) -> RemittanceIntentBean:
    remittanceVersionBean = RemittanceVersionDataStore.convertMongoToBean(intent.remittanceOrder.remittanceVersion)
    remittanceOrderBean = _getRemittanceOrderBean(intent.remittanceOrder, remittanceVersionBean)

    return RemittanceIntentBean(
        id=str(intent.id),
        companyId=str(intent.company.id),
        remittanceOrder=remittanceOrderBean,
        amount=intent.amount,
        currencyCode=intent.currencyCode,
        sourceInstruction=intent.sourceInstruction,
        destInstruction=intent.destInstruction,
        deps=list(intent.deps),
    )


def getAllLiabilityVersionsForPayrollObjectIds(
    payrollObjectIds: Sequence[str],
    payrollObjectType: str,
) -> Sequence[CompanyLiabilityVersionBean]:
    return _getAllLiabilityVersions(
        match={"payrollObjectId": {"$in": payrollObjectIds}, "payrollObjectType": payrollObjectType}
    )


def getLiabilityVersionsForCompanyId(
    companyId: str, createdAt: Optional[datetime] = None
) -> Sequence[CompanyLiabilityVersionBean]:
    matchQuery: dict[str, Any] = {"company": ObjectId(companyId)}
    afterGroupQuery: Optional[dict[str, Any]] = None
    if createdAt is not None:
        afterGroupQuery = {"createdAt": {"$gte": createdAt}}
    return _getAllLiabilityVersions(matchQuery, lastMatch=afterGroupQuery)


def getLiabilityVersionsForCompanyPayrollEntityId(
    cpeId: str, createdAt: Optional[datetime] = None
) -> Sequence[CompanyLiabilityVersionBean]:
    matchQuery: dict[str, Any] = {"companyPayrollEntity": ObjectId(cpeId)}
    afterGroupQuery: Optional[dict[str, Any]] = None
    if createdAt is not None:
        afterGroupQuery = {"createdAt": {"$gte": createdAt}}
    return _getAllLiabilityVersions(matchQuery, lastMatch=afterGroupQuery)


def _getAllLiabilityVersions(match: dict, lastMatch: Optional[dict] = None) -> Sequence[CompanyLiabilityVersionBean]:
    # todo: add this to the data store framework
    # Define the aggregation pipeline
    pipeline = [
        {"$match": match},
        {"$sort": {"version": -1}},
        {"$group": {"_id": "$payrollObjectId", "latestLiability": {"$first": "$$ROOT"}}},
        {"$replaceRoot": {"newRoot": "$latestLiability"}},
    ]
    if lastMatch:
        pipeline.append({"$match": lastMatch})

    # Execute the aggregation pipeline
    aggregatedResults = list(IPCompanyLiabilityVersion.objects.aggregate(*pipeline))

    # Format the results
    liabilitySetIds: set[str] = set(
        str(result["liabilitySet"]) for result in aggregatedResults if result.get("liabilitySet")
    )
    liabilitySets = getCompanyLiabilityTypeSetsById(list(liabilitySetIds))
    liabilitySetsById: dict[str, CompanyLiabilityTypeSet] = {
        str(liabilitySet.id): liabilitySet for liabilitySet in liabilitySets
    }
    return [
        CompanyLiabilityVersionBean(
            id=str(result["_id"]),
            companyId=str(result["company"]),
            countryCode=str(result.get("countryCode")),
            reasonCode=str(result["reasonCode"]),
            payrollObjectId=str(result["payrollObjectId"]),
            payrollObjectType=cast(LiabilityPayrollObjectTypes, str(result["payrollObjectType"])),
            liabilitySet=liabilitySetsById[str(result["liabilitySet"])],
            companyPayrollEntityId=str(result["companyPayrollEntity"]),
            checkDate=datetime.strptime(result["checkDate"], "%Y-%m-%d").date(),
        )
        for result in aggregatedResults
    ]


def getAllRemittanceVersionIdsByDueDate(
    startDateTime: datetime, endDateTime: Optional[datetime] = None
) -> Sequence[str]:
    match = {"dueDate": {"$gte": startDateTime}}
    if endDateTime:
        match["dueDate"]["$lte"] = endDateTime
    pipeline = [
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {
                    "payrollObjectId": "$payrollObjectId",
                    "payrollObjectType": "$payrollObjectType",
                    "liabilityCode": "$liabilityCode",
                },
                "latestRemittance": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latestRemittance"}},
        {"$match": match},
    ]
    return [str(result["_id"]) for result in IPRemittanceVersion.objects.aggregate(*pipeline)]


def getOrNoneRemittanceOrderForId(remittanceOrderId: str) -> Optional[RemittanceOrderBean]:
    remittanceOrderObj = IPRemittanceOrder.get_or_none(id=remittanceOrderId)
    if remittanceOrderObj is None:
        return None
    remittanceVersion = RemittanceVersionDataStore.getOrRaiseForId(str(remittanceOrderObj.remittanceVersion.id))
    return _getRemittanceOrderBean(remittanceOrderObj, remittanceVersion)


def saveRemittanceDebitConfig(debitConfig: RemittanceDebitConfig) -> RemittanceDebitConfig:
    oldObj = getLatestRemittanceDebitConfigForPayrollObjectId(
        debitConfig.payrollObjectId, debitConfig.payrollObjectType
    )
    if oldObj and oldObj.version is not None:
        version = oldObj.version + 1
    else:
        version = 0
    newObj: IPRemittanceDebitConfig = IPRemittanceDebitConfig.objects.create(
        version=version,
        companyId=debitConfig.companyId,
        payrollObjectId=debitConfig.payrollObjectId,
        payrollObjectType=debitConfig.payrollObjectType,
        doNotDebitTaxCodes=debitConfig.doNotDebitTaxCodes,
        doNotDebitLegalDeductionCodes=debitConfig.doNotDebitLegalDeductionCodes,
    )
    return _getRemittanceDebitConfigBean(newObj)


def getLatestRemittanceDebitConfigForPayrollObjectId(
    payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Optional[RemittanceDebitConfig]:
    obj: Optional[IPRemittanceDebitConfig] = (
        IPRemittanceDebitConfig.objects(payrollObjectId=payrollObjectId, payrollObjectType=payrollObjectType)
        .order_by("-version")
        .first()
    )
    if obj is None:
        return None
    return _getRemittanceDebitConfigBean(obj)


def getPaymentAttemptIdsForRemittanceIntentId(intentId: str) -> Sequence[str]:
    query = IPRemittancePaymentAttempt.objects(remittanceIntent=intentId).order_by("-version").only("id").as_pymongo()
    return [str(result["_id"]) for result in query]


def getRemittanceIntentStatusesForPayrollObject(
    companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Sequence[RemittanceIntentStatusBean]:
    intents = getRemittanceIntentsForPayrollObject(companyId, payrollObjectId, payrollObjectType)
    intentIds = [intent.id for intent in intents if intent.id is not None]
    return getLatestIntentStatusesForIntentIds(companyId, intentIds)


def getRemittanceIntentsForPayrollObject(
    companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Sequence[RemittanceIntentBean]:
    orders = getRemittanceOrdersForPayrollObjectId(companyId, payrollObjectId, payrollObjectType)
    return getRemittanceIntentsForRemittanceOrders(companyId, orders)


def getRemittanceOrdersForPayrollObjectId(
    companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Sequence[RemittanceOrderBean]:
    versions = getLatestRemittanceVersions(companyId, payrollObjectId, payrollObjectType)
    return getRemittanceOrdersForRemittanceVersions(companyId, versions)


def _getRemittancePayrollObjectSnapshotBean(obj: IPRemittancePayrollObjectSnapshot):
    return RemittancePayrollObjectSnapshot(
        companyId=str(obj.company.id),
        companyPayrollEntityId=str(obj.companyPayrollEntity.id),
        payrollObjectId=obj.payrollObjectId,
        payrollObjectType=cast(LiabilityPayrollObjectTypes, obj.payrollObjectType),
        liabilitySetId=str(obj.liabilitySet.id),
        version=obj.version,
        id=str(obj.id),
        effectiveDateTime=obj.effectiveDateTime,
    )


def getLatestRemittancePayrollObjectSnapshot(
    companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Optional[RemittancePayrollObjectSnapshot]:
    obj = (
        IPRemittancePayrollObjectSnapshot.objects(
            company=companyId, payrollObjectId=payrollObjectId, payrollObjectType=payrollObjectType
        )
        .order_by("-version")
        .first()
    )
    if obj is None:
        return None
    return _getRemittancePayrollObjectSnapshotBean(obj)


def getLatestRemittancePayrollObjectSnapshotVersion(
    companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes
) -> Optional[int]:
    obj = (
        IPRemittancePayrollObjectSnapshot.objects(
            company=companyId, payrollObjectId=payrollObjectId, payrollObjectType=payrollObjectType
        )
        .order_by("-version")
        .only("id", "version")
        .as_pymongo()
        .first()
    )
    if not obj:
        return None
    return obj["version"]


def writeRemittancePayrollObjectSnapshot(
    companyId: str,
    companyPayrollEntityId: str,
    payrollObjectId: str,
    payrollObjectType: LiabilityPayrollObjectTypes,
    liabilitySetId: str,
    effectiveDateTime: datetime,
) -> RemittancePayrollObjectSnapshot:
    latestVersion = getLatestRemittancePayrollObjectSnapshotVersion(companyId, payrollObjectId, payrollObjectType)
    nextVersion = latestVersion + 1 if latestVersion is not None else 0
    obj = IPRemittancePayrollObjectSnapshot.objects.create(
        company=companyId,
        companyPayrollEntity=companyPayrollEntityId,
        payrollObjectId=payrollObjectId,
        payrollObjectType=payrollObjectType,
        liabilitySet=liabilitySetId,
        version=nextVersion,
        effectiveDateTime=effectiveDateTime,
    )
    return _getRemittancePayrollObjectSnapshotBean(obj)


class RemittanceLiabilitySummaryDataStore:
    # todo: this class can be refactored into a Generic class useful to all data_store.py files.
    @staticmethod
    def getAllInProgressItemsAcrossCompanies() -> Sequence[RemittanceLiabilitySummary]:
        # pipeline to get all remittance liability summaries that are in progress
        pipeline = [
            {"$sort": {"version": -1}},
            {
                "$group": {
                    "_id": {
                        "payrollObjectId": "$payrollObjectId",
                        "payrollObjectType": "$payrollObjectType",
                        "liabilityCode": "$liabilityCode",
                    },
                    "latest": {"$first": "$$ROOT"},
                }
            },
            {"$replaceRoot": {"newRoot": "$latest"}},
            {"$match": {"state": RemittanceLiabilitySummaryState.IN_PROGRESS}},
        ]
        data = IPRemittanceLiabilitySummary.objects_across_company.aggregate(*pipeline)
        return [RemittanceLiabilitySummaryDataStore.getBeanFromPyMongo(d) for d in data]

    @staticmethod
    def getRecentlyUpdatedRemittanceVersionIdsInTimeRange(liabilityCode: str, startDate: datetime):
        # pipeline to get all remittance liability summaries that are in progress
        pipeline = [
            {"$match": {"liabilityCode": liabilityCode}},
            {"$sort": {"version": -1}},
            {
                "$group": {
                    "_id": {
                        "payrollObjectId": "$payrollObjectId",
                        "payrollObjectType": "$payrollObjectType",
                        "liabilityCode": "$liabilityCode",
                    },
                    "latest": {"$first": "$$ROOT"},
                }
            },
            {"$replaceRoot": {"newRoot": "$latest"}},
            {"$match": {"createdAt": {"$gte": startDate}}},
        ]
        data = IPRemittanceLiabilitySummary.objects_across_company.aggregate(*pipeline)
        return [RemittanceLiabilitySummaryDataStore.getBeanFromPyMongo(d) for d in data]

    @staticmethod
    def getOrNoneLatestVersion(
        companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes, liabilityCode: str
    ) -> Optional[RemittanceLiabilitySummary]:
        ipObject = (
            IPRemittanceLiabilitySummary.objects(
                company=companyId,
                payrollObjectId=payrollObjectId,
                payrollObjectType=payrollObjectType,
                liabilityCode=liabilityCode,
            )
            .order_by("-version")
            .first()
        )
        if ipObject is None:
            return None
        return RemittanceLiabilitySummaryDataStore.getBean(ipObject)

    @staticmethod
    def getNextVersionInt(
        companyId: str, payrollObjectId: str, payrollObjectType: LiabilityPayrollObjectTypes, liabilityCode: str
    ) -> int:
        ipObject = (
            IPRemittanceLiabilitySummary.objects(
                company=companyId,
                payrollObjectId=payrollObjectId,
                payrollObjectType=payrollObjectType,
                liabilityCode=liabilityCode,
            )
            .order_by("-version")
            .only("version")
            .as_pymongo()
            .first()
        )
        if ipObject is None:
            return 0
        return ipObject["version"] + 1

    @staticmethod
    def getBean(ipObject: IPRemittanceLiabilitySummary) -> RemittanceLiabilitySummary:
        return RemittanceLiabilitySummary(
            companyId=str(ipObject.company.id),
            payrollObjectId=ipObject.payrollObjectId,
            payrollObjectType=ipObject.payrollObjectType,
            liabilityCode=ipObject.liabilityCode,
            state=ipObject.state,
            version=ipObject.version,
            id=str(ipObject.id),
        )

    @staticmethod
    def getBeanFromPyMongo(data: dict[str, Any]) -> RemittanceLiabilitySummary:
        return RemittanceLiabilitySummary(
            companyId=str(data["company"]),
            payrollObjectId=data["payrollObjectId"],
            payrollObjectType=data["payrollObjectType"],
            liabilityCode=data["liabilityCode"],
            state=data["state"],
            version=data["version"],
            id=str(data["_id"]),
        )

    @staticmethod
    def onRemittanceVersionCreated(remittanceVersion: RemittanceVersionBean) -> None:
        RemittanceLiabilitySummaryDataStore.syncRemittanceVersion(remittanceVersion)

    @staticmethod
    def onRemittanceIntentStatusCreated(remittanceIntentStatus: RemittanceIntentStatusBean) -> None:
        if not remittanceIntentStatus.id:
            return

        intent = getOrNoneRemittanceIntentForId(intentId=remittanceIntentStatus.remittanceIntentId)
        if (not intent) or (not intent.remittanceOrder.remittanceVersionId):
            return

        if remittanceVersion := RemittanceVersionDataStore.getOrNoneForId(intent.remittanceOrder.remittanceVersionId):
            RemittanceLiabilitySummaryDataStore.syncRemittanceVersion(remittanceVersion)

    @staticmethod
    def syncRemittanceVersion(remittanceVersion: RemittanceVersionBean) -> None:
        orders = getRemittanceOrdersForRemittanceVersion(remittanceVersion)
        intents = getRemittanceIntentsForRemittanceOrders(remittanceVersion.companyId, orders)
        statuses = getLatestIntentStatusesForIntentIds(
            remittanceVersion.companyId, [intent.id for intent in intents if intent.id is not None]
        )
        RemittanceLiabilitySummaryDataStore.create(remittanceVersion, orders, intents, statuses)

    @staticmethod
    def create(
        remittanceVersion: RemittanceVersionBean,
        remittanceOrders: Sequence[RemittanceOrderBean],
        remittanceIntents: Sequence[RemittanceIntentBean],
        latestRemittanceIntentStatuses: Sequence[RemittanceIntentStatusBean],
    ) -> RemittanceLiabilitySummary:
        if not latestRemittanceIntentStatuses:
            state = RemittanceLiabilitySummaryState.COMPLETE
        elif all(IntentStatusUtil.isCompleted(intentStatus.status) for intentStatus in latestRemittanceIntentStatuses):
            state = RemittanceLiabilitySummaryState.COMPLETE
        else:
            state = RemittanceLiabilitySummaryState.IN_PROGRESS

        version = RemittanceLiabilitySummaryDataStore.getNextVersionInt(
            remittanceVersion.companyId,
            remittanceVersion.payrollObjectId,
            remittanceVersion.payrollObjectType,
            remittanceVersion.liabilityCode,
        )
        ipObject = IPRemittanceLiabilitySummary.objects.create(
            company=remittanceVersion.companyId,
            payrollObjectId=remittanceVersion.payrollObjectId,
            payrollObjectType=remittanceVersion.payrollObjectType,
            liabilityCode=remittanceVersion.liabilityCode,
            state=state,
            version=version,
        )
        return RemittanceLiabilitySummaryDataStore.getBean(ipObject)


def getLatestLiabilityVersionsForCompanyPayrollEntityIdAndCheckDate(
    companyPayrollEntityIds: Sequence[str],
    startDate: date,
    endDate: date,
) -> Sequence[CompanyLiabilityVersionBean]:
    companyPayrollEntityIds = [ObjectId(companyPayrollEntityId) for companyPayrollEntityId in companyPayrollEntityIds]
    return _getAllLiabilityVersions(
        match={
            "companyPayrollEntity": {"$in": companyPayrollEntityIds},
        },
        lastMatch={
            "checkDate": {"$gte": startDate.strftime("%Y-%m-%d"), "$lte": endDate.strftime("%Y-%m-%d")},
        },
    )


def _getRemittancePaymentActivityDisplayInfoBean(
    displayInfoDocument: IPRemittancePaymentActivityDisplayInfoDocument,
) -> RemittancePaymentActivityDisplayInfo:
    return RemittancePaymentActivityDisplayInfo(
        method=displayInfoDocument.method, agencyName=displayInfoDocument.agencyName
    )


def _getRemittancePaymentActivityDisplayInfoDocument(
    displayInfo: RemittancePaymentActivityDisplayInfo,
) -> IPRemittancePaymentActivityDisplayInfoDocument:
    return IPRemittancePaymentActivityDisplayInfoDocument(method=displayInfo.method, agencyName=displayInfo.agencyName)


def _getRemittancePaymentActivityBean(
    ipRemittancePaymentActivity: IPRemittancePaymentActivity,
) -> RemittancePaymentActivity:
    return RemittancePaymentActivity(
        id=str(ipRemittancePaymentActivity.id),
        companyId=str(ipRemittancePaymentActivity.company),
        groupKey=ipRemittancePaymentActivity.groupKey,
        groupItem=ipRemittancePaymentActivity.groupItem,
        liabilityCode=ipRemittancePaymentActivity.liabilityCode,
        payrollObjectId=ipRemittancePaymentActivity.payrollObjectId,
        payrollObjectType=ipRemittancePaymentActivity.payrollObjectType,
        status=ipRemittancePaymentActivity.status,
        companyPayrollEntityId=str(ipRemittancePaymentActivity.companyPayrollEntity.id),
        currencyCode=ipRemittancePaymentActivity.currencyCode,
        displayInfo=_getRemittancePaymentActivityDisplayInfoBean(ipRemittancePaymentActivity.displayInfo),
        amount=ipRemittancePaymentActivity.amount,
        createdAt=ipRemittancePaymentActivity.createdAt,
        version=ipRemittancePaymentActivity.version,
    )


def getOrNoneRemittancePaymentActivity(activityId: str) -> Optional[RemittancePaymentActivity]:
    ipActivity = IPRemittancePaymentActivity.objects.get_or_none(id=activityId)
    if ipActivity is None:
        return None
    return _getRemittancePaymentActivityBean(ipActivity)


def getOrNoneLatestRemittancePaymenentActivityForOrder(
    companyId: str, groupKey: str
) -> Optional[RemittancePaymentActivity]:
    return IPRemittancePaymentActivity.objects.filter(company=companyId, groupKey=groupKey).order_by("-version").first()


def getAllLatestRemittancePaymentActivityForCpe(
    cpe: CompanyPayrollEntity,
) -> list[RemittancePaymentActivity]:
    pipeline = [
        {"$match": {"companyPayrollEntity": ObjectId(cpe.id)}},
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {
                    "remittanceOrderId": "$remittanceOrderId",
                },
                "latest": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latest"}},
    ]
    ipActivitiesAggResults = list(IPRemittancePaymentActivity.objects.aggregate(*pipeline))

    listOfIds = [result["_id"] for result in ipActivitiesAggResults]
    ipActivities = IPRemittancePaymentActivity.objects.filter(id__in=listOfIds)
    return [_getRemittancePaymentActivityBean(ipActivity) for ipActivity in ipActivities]


def getAllLatestRemittancePaymentActivityForCompany(
    companyId: str,
) -> list[RemittancePaymentActivity]:
    pipeline = [
        {"$match": {"company": ObjectId(companyId)}},
        {"$sort": {"version": -1}},
        {
            "$group": {
                "_id": {
                    "remittanceOrderId": "$remittanceOrderId",
                },
                "latest": {"$first": "$$ROOT"},
            }
        },
        {"$replaceRoot": {"newRoot": "$latest"}},
    ]
    ipActivitiesAggResults = list(IPRemittancePaymentActivity.objects.aggregate(*pipeline))

    listOfIds = [result["_id"] for result in ipActivitiesAggResults]
    ipActivities = IPRemittancePaymentActivity.objects.filter(id__in=listOfIds)
    return [_getRemittancePaymentActivityBean(ipActivity) for ipActivity in ipActivities]


def getAllRemittancePaymentActivityForCompanyByGroupKey(companyId: str) -> dict[str, list[RemittancePaymentActivity]]:
    remittanceActivityObjs = IPRemittancePaymentActivity.objects.filter(company=companyId)

    groupKeyToActivities: dict[str, list[RemittancePaymentActivity]] = defaultdict(list)
    for activityObj in remittanceActivityObjs:
        activity = _getRemittancePaymentActivityBean(activityObj)
        groupKeyToActivities[activity.groupKey].append(activity)

    return groupKeyToActivities


def getOrNoneRemittancePaymentActivityGroup(
    companyId: str, groupKey: str, groupItem: str, status: RemittancePaymentActivityStatus
) -> Optional[RemittancePaymentActivity]:
    ipActivity = IPRemittancePaymentActivity.objects.get_or_none(
        company=companyId, groupKey=groupKey, groupItem=groupItem, status=status
    )

    if not ipActivity:
        return None
    return _getRemittancePaymentActivityBean(ipActivity)


def createRemittancePaymentActivity(activity: RemittancePaymentActivity) -> RemittancePaymentActivity:
    latestPaymentActivity = getOrNoneLatestRemittancePaymenentActivityForOrder(activity.companyId, activity.groupKey)
    if latestPaymentActivity is None:
        version = 0
    else:
        version = latestPaymentActivity.version + 1

    ipRemittancePaymentActivity = IPRemittancePaymentActivity.objects.create(
        company=activity.companyId,
        groupKey=activity.groupKey,
        groupItem=activity.groupItem,
        payrollObjectId=activity.payrollObjectId,
        payrollObjectType=activity.payrollObjectType,
        liabilityCode=activity.liabilityCode,
        status=activity.status,
        companyPayrollEntity=activity.companyPayrollEntityId,
        currencyCode=activity.currencyCode,
        displayInfo=_getRemittancePaymentActivityDisplayInfoDocument(activity.displayInfo),
        amount=activity.amount,
        version=version,
    )

    return _getRemittancePaymentActivityBean(ipRemittancePaymentActivity)


def _toRemittanceFFRequestBatch(batch: IPRemittanceFFRequestBatch) -> RemittanceFFRequestBatch:
    return RemittanceFFRequestBatch(
        id=str(batch.id),
        groupKey=BatchGroupKey(batch.groupKey),
        index=batch.index,
        liabilityCode=batch.liabilityCode,
        sourceLocation=batch.sourceLocation,
        destLocation=batch.destLocation,
        destCurrencyCode=batch.destCurrencyCode,
        destCountryCode=batch.destCountryCode,
        expectedDepartureDate=batch.expectedDepartureDate,
        expectedCheckDate=batch.expectedCheckDate,
    )


def getOrNoneAssignedBatchForRequestId(ffRequestId: str) -> Optional[RemittanceFFRequestBatch]:
    ipAssignment = IPRemittanceFFRequestBatchAssignment.get_or_none(ffRequest=ffRequestId)
    if ipAssignment is None:
        return None
    return _toRemittanceFFRequestBatch(ipAssignment.batch)


def _toRemittanceFFRequestBatchStatus(ipStatus: IPRemittanceFFRequestBatchStatus) -> RemittanceFFRequestBatchStatus:
    return RemittanceFFRequestBatchStatus(
        id=str(ipStatus.id),
        batchId=str(ipStatus.ffRequestBatch.id),
        status=ipStatus.status,
        reason=ipStatus.reason,
        counter=ipStatus.counter,
    )


def getLatestBatchStatus(batch: RemittanceFFRequestBatch) -> RemittanceFFRequestBatchStatus:
    ipStatus = _getLatestIPBatchStatusForBatchId(batch.id)
    return _toRemittanceFFRequestBatchStatus(ipStatus)


def _getLatestIPBatchStatusForBatchId(batchId: str | ObjectId) -> IPRemittanceFFRequestBatchStatus:
    ipStatus = IPRemittanceFFRequestBatchStatus.objects.filter(ffRequestBatch=batchId).order_by("-counter").first()
    if not ipStatus:
        # always require a status
        raise IntentStatusNotFoundException(f"Status not found for batchId {batchId}")
    return ipStatus


def _writeIPBatchStatusForBatchId(
    batchId: str | ObjectId, newStatus: IntentStatus, counter: int, reason: str = ""
) -> IPRemittanceFFRequestBatchStatus:
    return IPRemittanceFFRequestBatchStatus.objects.create(
        ffRequestBatch=batchId, status=newStatus, reason=reason, counter=counter
    )


def writeBatchStatus(
    batch: RemittanceFFRequestBatch, newStatus: IntentStatus, counter: int, reason: str = ""
) -> RemittanceFFRequestBatchStatus:
    ipStatus = _writeIPBatchStatusForBatchId(batch.id, newStatus, counter, reason)
    return _toRemittanceFFRequestBatchStatus(ipStatus)


def getOrCreateNextBatchForGroupKey(bean: RemittanceFFRequestBatch) -> RemittanceFFRequestBatch:
    ipBatch = IPRemittanceFFRequestBatch.objects.filter(groupKey=bean.groupKey).order_by("-index").first()
    status = _getLatestIPBatchStatusForBatchId(ipBatch.id) if ipBatch else None

    if ipBatch and status and not IntentStatusUtil.isAutomaticRetryAllowed(status.status):
        return _toRemittanceFFRequestBatch(ipBatch)

    nextIndex = ipBatch.index + 1 if ipBatch else 0
    nextBatch = IPRemittanceFFRequestBatch.objects.create(
        groupKey=bean.groupKey,
        index=nextIndex,
        liabilityCode=bean.liabilityCode,
        sourceLocation=bean.sourceLocation,
        destLocation=bean.destLocation,
        destCurrencyCode=bean.destCurrencyCode,
        destCountryCode=bean.destCountryCode,
        expectedDepartureDate=bean.expectedDepartureDate,
        expectedCheckDate=bean.expectedCheckDate,
    )
    _writeIPBatchStatusForBatchId(nextBatch.id, IntentStatus.CREATED, counter=0)
    return _toRemittanceFFRequestBatch(nextBatch)


def getOrNoneRemittanceFFRequestBatch(batchId: str) -> Optional[RemittanceFFRequestBatch]:
    ipBatch = IPRemittanceFFRequestBatch.get_or_none(id=batchId)
    if ipBatch is None:
        return None
    return _toRemittanceFFRequestBatch(ipBatch)


def _toRemittanceFFRequestBatchAssignment(
    ipAssignment: IPRemittanceFFRequestBatchAssignment,
) -> RemittanceFFRequestBatchAssignmentBean:
    return RemittanceFFRequestBatchAssignmentBean(
        batchId=str(ipAssignment.batch.id),
        ffRequestId=str(ipAssignment.ffRequest.id),
    )


def assignFFRequestToBatch(
    ffRequest: RemittanceIntentFFRequestBean, batch: RemittanceFFRequestBatch
) -> RemittanceFFRequestBatchAssignmentBean:
    ipAssignment = IPRemittanceFFRequestBatchAssignment.get_or_create(batch=batch.id, ffRequest=ffRequest.id)
    return _toRemittanceFFRequestBatchAssignment(ipAssignment)


def getOrNoneRemittanceIntentForFFRequest(ffRequest: RemittanceIntentFFRequestBean) -> Optional[RemittanceIntentBean]:
    ipFFRequest = IPRemittanceFulfillmentRequest.get_or_none(id=ffRequest.id)
    if ipFFRequest is None:
        return None
    return _getRemittanceIntentBean(ipFFRequest.intent)


def getFFRequestBatchIdsReadyForFulfillment(countryCode: str, startDate: date, endDate: date) -> Sequence[str]:
    pymongoIds = (
        IPRemittanceFFRequestBatch.objects(
            destCountryCode=countryCode,
            expectedDepartureDate__gte=startDate,
            expectedDepartureDate__lte=endDate,
        )
        .only("id")
        .as_pymongo()
    )
    return [str(result["_id"]) for result in pymongoIds]


def getCountryCodeFromIntentId(intentId: str) -> str:
    # todo: fix the data model to avoid this deep query
    return IPRemittanceIntent.objects.get(id=intentId).remittanceOrder.remittanceVersion.liabilityVersion.countryCode


def getCheckDateFromIntentId(intentId: str) -> date:
    # todo: fix the data model to avoid this deep query
    return IPRemittanceIntent.objects.get(id=intentId).remittanceOrder.remittanceVersion.liabilityVersion.checkDate


def getCompanyPayrollEntityIdFromIntentId(intentId: str) -> str:
    return str(
        IPRemittanceIntent.objects.get(
            id=intentId
        ).remittanceOrder.remittanceVersion.liabilityVersion.companyPayrollEntity.id
    )
