diff --git a/.storybook/stories/BorrowerProfile/LoanProgress.stories.js b/.storybook/stories/BorrowerProfile/LoanProgress.stories.js index e490c47be6e..0b5809c9ce8 100644 --- a/.storybook/stories/BorrowerProfile/LoanProgress.stories.js +++ b/.storybook/stories/BorrowerProfile/LoanProgress.stories.js @@ -88,6 +88,7 @@ export const PayingBack = () => ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ template: ` ({ `, }); +export const MissingStatusLabel = () => ({ + components: { LoanProgress }, + template: ` + + `, +}); +MissingStatusLabel.storyName = 'Missing Status Label (falls back to the raw status)'; + export const Loading = () => ({ components: { LoanProgress }, template: ` diff --git a/src/components/BorrowerProfile/LoanProgress.vue b/src/components/BorrowerProfile/LoanProgress.vue index 2a3726fddb2..1c6ab71c555 100644 --- a/src/components/BorrowerProfile/LoanProgress.vue +++ b/src/components/BorrowerProfile/LoanProgress.vue @@ -116,7 +116,7 @@
- {{ isDelinquent ? 'Paying back delinquent' : 'Paying back' }} + {{ displayStatusLabel }}

@@ -154,7 +154,7 @@

- {{ statusLabel }} + {{ displayStatusLabel }}

@@ -192,13 +192,9 @@ export default { type: String, default: 'fundraising', }, - isDelinquent: { - type: Boolean, - default: false, - }, - hasCurrencyExchangeLoss: { - type: Boolean, - default: false, + statusLabel: { + type: String, + default: '', }, numberOfLenders: { type: Number, @@ -246,20 +242,8 @@ export default { routeId() { return this.loanId ? this.loanId : this.$route.params.id; }, - statusLabel() { - if (this.loanStatus === 'ended' && this.hasCurrencyExchangeLoss) { - return 'Repaid with currency loss'; - } - const labels = { - ended: 'Repaid', - defaulted: 'Defaulted', - refunded: 'Refunded', - inactiveExpired: 'Inactive', - reviewed: 'Under review', - deleted: 'Deleted', - issue: 'Issue', - }; - return labels[this.loanStatus] || this.loanStatus; + displayStatusLabel() { + return this.statusLabel || this.loanStatus; }, }, }; diff --git a/src/components/BorrowerProfile/MinimalBorrowerProfile.vue b/src/components/BorrowerProfile/MinimalBorrowerProfile.vue index e85a152b3f9..8200a4d3675 100644 --- a/src/components/BorrowerProfile/MinimalBorrowerProfile.vue +++ b/src/components/BorrowerProfile/MinimalBorrowerProfile.vue @@ -48,6 +48,7 @@ :progress-percent="progressPercent" :loading="isSummaryLoading" :loan-status="loanStatus" + :status-label="statusLabel" :is-live-loan-ad="isLiveLoanAd" />
@@ -123,6 +124,7 @@ export const minimalProfileFragment = gql`fragment minimalProfileFields on LoanB id name status + statusLabel use anonymizationLevel loanAmount @@ -242,10 +244,8 @@ export default { rows: null, isVisitor: true, loanRowsCount: 4, - // Initialize from the loan prop (populated by the parent page's routingQuery, - // which carries shareMetaFragment fields including name and country). Without - // this, SSR renders with loanData={} and head() produces the broken - // "undefined from undefined's loan has been funded!" title + // Seeded from the loan prop, which the parent page reads back from this + // component's own prefetched query. loanData: this.loan?.id ? { ...this.loan } : {}, }; }, @@ -273,6 +273,9 @@ export default { } return this.loanData?.status ?? 'funded'; }, + statusLabel() { + return this.loanData?.statusLabel ?? ''; + }, progressPercent() { if (this.loanStatus === 'funded') { return 1; diff --git a/src/components/BorrowerProfile/SummaryCard.vue b/src/components/BorrowerProfile/SummaryCard.vue index 14ce5c38e08..c5929d4c57f 100644 --- a/src/components/BorrowerProfile/SummaryCard.vue +++ b/src/components/BorrowerProfile/SummaryCard.vue @@ -87,8 +87,7 @@ :progress-percent="effectiveProgressPercent" :time-left="timeLeft" :loan-status="inPfp ? 'pfp' : status" - :is-delinquent="delinquent" - :has-currency-exchange-loss="hasCurrencyExchangeLoss" + :status-label="statusLabel" :number-of-lenders="numLenders" :pfp-min-lenders="pfpMinLenders" :loading="isLoading" @@ -167,8 +166,7 @@ export const summaryCardFragment = gql`fragment summaryCardFields on LoanBasic { } name status - delinquent - hasCurrencyExchangeLossLenders + statusLabel use anonymizationLevel borrowerCount @@ -286,11 +284,8 @@ export default { status() { return this.loan?.status ?? ''; }, - delinquent() { - return this.loan?.delinquent ?? false; - }, - hasCurrencyExchangeLoss() { - return this.loan?.hasCurrencyExchangeLossLenders ?? false; + statusLabel() { + return this.loan?.statusLabel ?? ''; }, use() { return this.loan?.fullLoanUse ?? ''; diff --git a/src/pages/BorrowerProfile/BorrowerProfile.vue b/src/pages/BorrowerProfile/BorrowerProfile.vue index b7c9e22ec78..d4b9c08fba4 100644 --- a/src/pages/BorrowerProfile/BorrowerProfile.vue +++ b/src/pages/BorrowerProfile/BorrowerProfile.vue @@ -350,24 +350,32 @@ export default { }, result(result) { const routingLoan = result?.data?.lend?.loan ?? {}; - // Prefer the enriched full-profile entry; minimal-view paths fall back to routingLoan below. - let fullLoan = null; - let fullMy = null; + const isVolunteer = !!result?.data?.my?.userAccount?.volunteerId; + // Read back whichever child query preFetch ran. + const childQuery = showFullView( + routingLoan.status, + Number(routingLoan.unreservedAmount ?? 0), + routingLoan.userProperties?.isPrivileged ?? false, + isVolunteer, + this.$route?.query, + ) ? fullProfileQuery : minimalProfileQuery; + let childLoan = null; + let childMy = null; if (routingLoan.id) { try { const cached = this.apollo.readQuery({ - query: fullProfileQuery, + query: childQuery, variables: { loanId: routingLoan.id }, }); - fullLoan = cached?.lend?.loan; - fullMy = cached?.my; + childLoan = cached?.lend?.loan; + childMy = cached?.my; } catch { // Not in cache; fall back below. } } - this.loan = fullLoan ?? routingLoan; + this.loan = childLoan ?? routingLoan; this.routingLoan = routingLoan; - this.isVolunteer = !!result?.data?.my?.userAccount?.volunteerId; + this.isVolunteer = isVolunteer; this.inviterName = this.inviterIsGuestOrAnonymous ? '' : result?.data?.community?.lender?.name ?? ''; this.itemsInBasket = result?.data?.shop?.basket?.items?.values ?? []; @@ -377,7 +385,7 @@ export default { // SSR initial rail state from the account preference (localStorage is reconciled // client-side in FullBorrowerProfile); null for anon, so this stays false. this.initialShowDetailsInRail = resolveRailPreference({ - accountPref: readAccountRailPreference(fullMy?.userPreferences), + accountPref: readAccountRailPreference(childMy?.userPreferences), local: null, }); }, diff --git a/test/unit/specs/pages/BorrowerProfile/BorrowerProfile.spec.js b/test/unit/specs/pages/BorrowerProfile/BorrowerProfile.spec.js index 4820dabcb66..8697bd15780 100644 --- a/test/unit/specs/pages/BorrowerProfile/BorrowerProfile.spec.js +++ b/test/unit/specs/pages/BorrowerProfile/BorrowerProfile.spec.js @@ -137,3 +137,110 @@ describe('BorrowerProfile.apollo.preFetch', () => { expect(getChildProfileOperationName(client)).toBe('minimalBorrowerProfileData'); }); }); + +describe('BorrowerProfile.apollo.result', () => { + const makeRoutingLoan = (status, { isPrivileged = false, unreservedAmount = '400' } = {}) => ({ + id: 12345, + status, + unreservedAmount, + userProperties: { isPrivileged }, + }); + + const makeCtx = ({ cached = null, routeQuery = {} } = {}) => ({ + apollo: { + readQuery: vi.fn(() => cached), + }, + $route: { query: routeQuery }, + inviterIsGuestOrAnonymous: false, + expRegionList: [], + }); + + const invokeResult = (ctx, loan, { volunteerId = null } = {}) => { + BorrowerProfile.apollo.result.call(ctx, { + data: { lend: { loan }, my: { userAccount: { volunteerId } } }, + }); + }; + + const readQueryOperation = ctx => getOperationName(ctx.apollo.readQuery.mock.calls[0][0].query); + + it.each([ + { + title: 'a fundraising loan with shares remaining', + loan: makeRoutingLoan('fundraising'), + options: {}, + routeQuery: {}, + expected: 'fullBorrowerProfileData', + }, + { + title: 'a fully-reserved funded loan', + loan: makeRoutingLoan('funded', { unreservedAmount: '0' }), + options: {}, + routeQuery: {}, + expected: 'minimalBorrowerProfileData', + }, + { + title: 'a privileged viewer on a reviewed loan', + loan: makeRoutingLoan('reviewed', { isPrivileged: true }), + options: {}, + routeQuery: {}, + expected: 'fullBorrowerProfileData', + }, + { + title: 'a volunteer viewer on a fully-reserved loan', + loan: makeRoutingLoan('funded', { unreservedAmount: '0' }), + options: { volunteerId: 987 }, + routeQuery: {}, + expected: 'fullBorrowerProfileData', + }, + { + title: 'a minimal=false override on a fully-reserved loan', + loan: makeRoutingLoan('funded', { unreservedAmount: '0' }), + options: {}, + routeQuery: { minimal: 'false' }, + expected: 'fullBorrowerProfileData', + }, + ])('reads back $expected for $title', ({ + loan, options, routeQuery, expected, + }) => { + const ctx = makeCtx({ routeQuery }); + + invokeResult(ctx, loan, options); + + expect(readQueryOperation(ctx)).toBe(expected); + }); + + it('surfaces the cached minimal-profile loan rather than the routing loan', () => { + const loan = makeRoutingLoan('ended', { unreservedAmount: '0' }); + const ctx = makeCtx({ + cached: { lend: { loan: { id: 12345, status: 'ended', statusLabel: 'Repaid' } } }, + }); + + invokeResult(ctx, loan); + + expect(ctx.loan.statusLabel).toBe('Repaid'); + expect(ctx.routingLoan).toEqual(loan); + }); + + it('carries the cached account rail preference into the SSR initial state', () => { + const loan = makeRoutingLoan('fundraising'); + const ctx = makeCtx({ + cached: { + lend: { loan: { id: 12345, status: 'fundraising' } }, + my: { id: 1, userPreferences: { id: 2, preferences: '{"showLoanDetailsInRail":true}' } }, + }, + }); + + invokeResult(ctx, loan); + + expect(ctx.initialShowDetailsInRail).toBe(true); + }); + + it('falls back to the routing loan when the child query is not cached', () => { + const loan = makeRoutingLoan('ended', { unreservedAmount: '0' }); + const ctx = makeCtx(); + + invokeResult(ctx, loan); + + expect(ctx.loan).toEqual(loan); + }); +});