Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix new individual invoice after one is paid as a business #54138

Merged
merged 23 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bac1302
Failing test only returns invoice chat when receiver type matches
neil-marcellini Dec 13, 2024
59d79fa
Fix test, require invoice receiver to match
neil-marcellini Dec 13, 2024
0afdca3
WIP test getSendMoneyInvoiceInformation
neil-marcellini Dec 13, 2024
3054897
WIP test via sendInvoice
neil-marcellini Dec 13, 2024
401f5de
Fix test, remove extra expected param
neil-marcellini Dec 13, 2024
3d11b6c
Pass proper invoice receiver type
neil-marcellini Dec 13, 2024
03c9bf9
WIP fix some types
neil-marcellini Dec 13, 2024
0c6e50d
Add doc comment to make function more clear
neil-marcellini Dec 14, 2024
a2bbb54
Fix receiver type getting B2B invoice room
neil-marcellini Dec 14, 2024
410868e
Major clean up of tests and types and lint
neil-marcellini Dec 14, 2024
57ba052
Jest matcher returns any so cast to fix type error
neil-marcellini Dec 14, 2024
e6d5a92
Undo edge case type changes
neil-marcellini Dec 14, 2024
2ada6b8
More type fixes
neil-marcellini Dec 14, 2024
3e4e033
Fix lint
neil-marcellini Dec 14, 2024
4516d8f
Fix test, bad import path
neil-marcellini Dec 14, 2024
17d298a
Fix accidentally changed comment
neil-marcellini Dec 17, 2024
5fefb6c
Merge branch 'main' into neil-fix-getInvoiceChatByParticipants
neil-marcellini Dec 17, 2024
b62ad1b
Fix style
neil-marcellini Dec 17, 2024
7275ccf
Merge main to fix conflicts
neil-marcellini Dec 23, 2024
68cabf2
Merge main to fix types
neil-marcellini Dec 24, 2024
b89a319
Remove field no longer present on type
neil-marcellini Dec 24, 2024
55572c3
Remove accidentally added client only Report key state
neil-marcellini Dec 24, 2024
3e56f47
Merge branch 'main' into neil-fix-getInvoiceChatByParticipants
neil-marcellini Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
case 'iouReportID':
case 'preexistingReportID':
case 'private_isArchived':
case 'welcomeMessage':
return validateString(value);
case 'hasOutstandingChildRequest':
case 'hasOutstandingChildTask':
Expand Down Expand Up @@ -513,6 +514,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION,
pendingFields: 'object',
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE,
permissions: 'array',
},
'number',
);
Expand Down Expand Up @@ -621,6 +623,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
partial: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION,
preview: CONST.RED_BRICK_ROAD_PENDING_ACTION,
welcomeMessage: CONST.RED_BRICK_ROAD_PENDING_ACTION,
});
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/Ony
import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
import type {ConnectionName} from '@src/types/onyx/Policy';
import type {NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {InvoiceReceiverType, NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction';
import type {PendingChatMember} from '@src/types/onyx/ReportMetadata';
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
Expand Down Expand Up @@ -6774,14 +6774,15 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec
/**
* Attempts to find an invoice chat report in onyx with the provided policyID and receiverID.
*/
function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection<Report> = allReports): OnyxEntry<Report> {
function getInvoiceChatByParticipants(receiverID: string | number, receiverType: InvoiceReceiverType, policyID?: string, reports: OnyxCollection<Report> = allReports): OnyxEntry<Report> {
return Object.values(reports ?? {}).find((report) => {
if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) {
return false;
}

const isSameReceiver =
report.invoiceReceiver &&
report.invoiceReceiver.type === receiverType &&
(('accountID' in report.invoiceReceiver && report.invoiceReceiver.accountID === receiverID) ||
('policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === receiverID));

Expand Down
28 changes: 25 additions & 3 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import GoogleTagManager from '@libs/GoogleTagManager';
import * as IOUUtils from '@libs/IOUUtils';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
import Navigation from '@libs/Navigation/Navigation';
import * as NextStepUtils from '@libs/NextStepUtils';
Expand All @@ -65,6 +66,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {Attendee, Participant, Split} from '@src/types/onyx/IOU';
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report';
import type ReportAction from '@src/types/onyx/ReportAction';
import type {OnyxData} from '@src/types/onyx/Request';
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
Expand Down Expand Up @@ -2071,6 +2073,25 @@ function getDeleteTrackExpenseInformation(
return {parameters, optimisticData, successData, failureData, shouldDeleteTransactionThread, chatReport};
}

/**
* Get the invoice receiver type based on the receiver participant.
* @param receiverParticipant The participant who will receive the invoice or the invoice receiver object directly.
* @returns The invoice receiver type.
*/
function getReceiverType(receiverParticipant: Participant | InvoiceReceiver | undefined): InvoiceReceiverType {
if (!receiverParticipant) {
Log.warn('getReceiverType called with no receiverParticipant');
return CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
}
if ('type' in receiverParticipant && receiverParticipant.type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess optional chaining was troubling here 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the type narrowing seems to be necessary because the receiverParticipant is a combination of two types.

return receiverParticipant.type;
}
if ('policyID' in receiverParticipant && receiverParticipant.policyID) {
return CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS;
}
return CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
}

/** Gathers all the data needed to create an invoice. */
function getSendInvoiceInformation(
transaction: OnyxEntry<OnyxTypes.Transaction>,
Expand All @@ -2086,17 +2107,18 @@ function getSendInvoiceInformation(
const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {};
const trimmedComment = (comment?.comment ?? '').trim();
const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID ?? '-1';
const receiverParticipant = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver;
const receiverParticipant: Participant | InvoiceReceiver | undefined = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver;
const receiverAccountID = receiverParticipant && 'accountID' in receiverParticipant && receiverParticipant.accountID ? receiverParticipant.accountID : -1;
let receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID);
let optimisticPersonalDetailListAction = {};
const receiverType = getReceiverType(receiverParticipant);

// STEP 1: Get existing chat report OR build a new optimistic one
let isNewChatReport = false;
let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null;

if (!chatReport) {
chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID) ?? null;
chatReport = ReportUtils.getInvoiceChatByParticipants(receiverAccountID, receiverType, senderWorkspaceID) ?? null;
}

if (!chatReport) {
Expand Down Expand Up @@ -6896,7 +6918,7 @@ function getPayMoneyRequestParams(
}

if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && activePolicyID) {
const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID);
const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport.policyID);
if (existingB2BInvoiceRoom) {
chatReport = existingB2BInvoiceRoom;
}
Expand Down
9 changes: 9 additions & 0 deletions src/types/onyx/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1491,9 +1491,15 @@ type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Account balance */
stripeConnectAccountBalance?: number;

/** AccountID */
stripeConnectAccountID?: string;

/** bankAccountID of selected BBA for payouts */
transferBankAccountID?: number;
};

/** The markUp */
markUp?: number;
}>;

/** Names of policy features */
Expand Down Expand Up @@ -1630,6 +1636,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
harvesting?: {
/** Whether the scheduled submit is enabled */
enabled: boolean;

/** The ID of the Bedrock job that runs harvesting */
jobID?: number;
};

/** Whether the self approval or submitting is enabled */
Expand Down
11 changes: 10 additions & 1 deletion src/types/onyx/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type Participant = OnyxCommon.OnyxValueWithOfflineFeedback<{

/** Whether the participant is visible in the report */
notificationPreference: NotificationPreference;

/** Permissions granted to the participant */
permissions?: Array<ValueOf<typeof CONST.REPORT.PERMISSIONS>>;
}>;

/** Types of invoice receivers in a report */
Expand All @@ -49,6 +52,9 @@ type InvoiceReceiver =
policyID: string;
};

/** Type of invoice receiver */
type InvoiceReceiverType = InvoiceReceiver['type'];

/** Record of report participants, indexed by their accountID */
type Participants = Record<number, Participant>;

Expand Down Expand Up @@ -217,6 +223,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the report is archived */
// eslint-disable-next-line @typescript-eslint/naming-convention
private_isArchived?: string;

/** The report's welcome message */
welcomeMessage?: string;
},
'addWorkspaceRoom' | 'avatar' | 'createChat' | 'partial' | 'reimbursed' | 'preview'
>;
Expand All @@ -226,4 +235,4 @@ type ReportCollectionDataSet = CollectionDataSet<typeof ONYXKEYS.COLLECTION.REPO

export default Report;

export type {NotificationPreference, RoomVisibility, WriteCapability, Note, ReportCollectionDataSet, Participant, Participants, InvoiceReceiver};
export type {NotificationPreference, RoomVisibility, WriteCapability, Note, ReportCollectionDataSet, Participant, Participants, InvoiceReceiver, InvoiceReceiverType};
1 change: 1 addition & 0 deletions src/types/utils/whitelistedReportKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback<
};
// eslint-disable-next-line @typescript-eslint/naming-convention
private_isArchived: unknown;
welcomeMessage: unknown;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we add these keys in the OpenApp call ?:

Screenshot 2024-12-24 at 10 37 43 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is strange. The whole thing is pretty confusing. It's just listing all the fields in the report and asserting that they're in the report type. I don't find any value in that because it's totally circular.

I found the related issue, and the goal there is to move client only keys out of the report type. So now I understand the purpose. It also doesn't really make sense to limit that to the OpenApp call, I think OpenReport is also a valid way to tell which keys come from the server. I checked the response and I see that welcomeMessage is returned but state is not, so I'll remove state.

Thanks for pointing this out.

},
PolicyReportField['fieldID']
>;
Expand Down
30 changes: 30 additions & 0 deletions tests/actions/IOUTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as PolicyActions from '@src/libs/actions/Policy/Policy';
import * as Report from '@src/libs/actions/Report';
import * as ReportActions from '@src/libs/actions/ReportActions';
import * as User from '@src/libs/actions/User';
import * as API from '@src/libs/API';
import DateUtils from '@src/libs/DateUtils';
import * as Localize from '@src/libs/Localize';
import * as NumberUtils from '@src/libs/NumberUtils';
Expand All @@ -24,6 +25,8 @@ import type {ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'
import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction';
import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as InvoiceData from '../data/Invoice';
import type {InvoiceTestData} from '../data/Invoice';
import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
import createRandomReport from '../utils/collections/reports';
import createRandomTransaction from '../utils/collections/transaction';
Expand Down Expand Up @@ -3454,6 +3457,33 @@ describe('actions/IOU', () => {
});

describe('sendInvoice', () => {
it('creates a new invoice chat when one has been converted from individual to business', async () => {
// Mock API.write for this test
const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());

// Given a convertedInvoiceReport is stored in Onyx
const {policy, transaction, convertedInvoiceChat}: InvoiceTestData = InvoiceData;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat?.reportID}`, convertedInvoiceChat ?? {});

// And data for when a new invoice is sent to a user
const currentUserAccountID = 32;
const companyName = 'b1-53019';
const companyWebsite = 'https://www.53019.com';

// When the user sends a new invoice to an individual
IOU.sendInvoice(currentUserAccountID, transaction, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite);

// Then a new invoice chat is created instead of incorrectly using the invoice chat which has been converted from individual to business
expect(writeSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID) as string,
}),
expect.anything(),
);
writeSpy.mockRestore();
});

it('should not clear transaction pending action when send invoice fails', async () => {
// Given a send invoice request
mockFetch?.pause?.();
Expand Down
Loading
Loading