Repository: bloom-housing/bloom
Pull request #4835: fix: listing status sort bug
Author: login: mcgarrye
Labels: name: bug
description: Something isn't working
Created: 2025-04-23T20:00:09Z | Merged: 2025-04-28T21:13:01Z

This PR addresses [#(4817)](https://github.com/bloom-housing/bloom/issues/4817)

- [x] Addresses the issue in full
- [ ] Addresses only certain aspects of the issue

## Description

When sorting and paginating on listings with OrderBy set to `status`, Prisma may return repeats on different pages since `status` is not unique and can include a large number of listings, causing some listings not to appear at all.

The solution is to include a secondary sort by name. This way it will sort by status and then subsort by name.

## How Can This Be Tested/Reviewed?

Play around with sorting on the partners listing view. Have more than 8 listings with a single status to make sure issue is resolved.

## Author Checklist:

- [x] Added QA notes to the issue with applicable URLs
- [x] Reviewed in a desktop view
- [ ] Reviewed in a mobile view
- [ ] Reviewed considering accessibility
- [x] Added tests covering the changes
- [ ] Made corresponding changes to the documentation
- [ ] Ran `yarn generate:client` and/or created a migration when required

## Review Process:

- Read and understand the issue
- Ensure the author has added QA notes
- Review the code itself from a style point of view
- Pull the changes down locally and test that the acceptance criteria is met
- Either (1) explicitly ask a clarifying question, (2) request changes, or (3) approve the PR, even if there are very small remaining changes, if you don't need to re-review after the updates



Diff:
diff --git a/api/src/utilities/build-order-by.ts b/api/src/utilities/build-order-by.ts
index 839cb2d07f7..bb3548fb01b 100644
--- a/api/src/utilities/build-order-by.ts
+++ b/api/src/utilities/build-order-by.ts
@@ -15,6 +15,9 @@ export const buildOrderByForListings = (
     return undefined;
   }
 
+  orderBy.push(ListingOrderByKeys.name);
+  orderDir.push(OrderByEnum.ASC);
+
   return orderBy.map((param, index) => {
     switch (param) {
       case ListingOrderByKeys.mostRecentlyUpdated:
diff --git a/api/test/unit/services/listing.service.spec.ts b/api/test/unit/services/listing.service.spec.ts
index 3689b37e32a..51c2cb7bee9 100644
--- a/api/test/unit/services/listing.service.spec.ts
+++ b/api/test/unit/services/listing.service.spec.ts
@@ -702,6 +702,9 @@ describe('Testing listing service', () => {
           {
             name: 'asc',
           },
+          {
+            name: 'asc',
+          },
         ],
         where: {
           AND: [
@@ -863,6 +866,9 @@ describe('Testing listing service', () => {
           {
             name: 'asc',
           },
+          {
+            name: 'asc',
+          },
         ],
         where: {
           AND: [],
@@ -1203,6 +1209,9 @@ describe('Testing listing service', () => {
           {
             name: 'asc',
           },
+          {
+            name: 'asc',
+          },
         ],
         where: {
           AND: [
diff --git a/api/test/unit/utilities/build-order-by.spec.ts b/api/test/unit/utilities/build-order-by.spec.ts
index eb7ddfb9835..8904e97fb0d 100644
--- a/api/test/unit/utilities/build-order-by.spec.ts
+++ b/api/test/unit/utilities/build-order-by.spec.ts
@@ -1,11 +1,114 @@
+import { ListingOrderByKeys } from '../../../src/enums/listings/order-by-enum';
 import { OrderByEnum } from '../../../src/enums/shared/order-by-enum';
-import { buildOrderBy } from '../../../src/utilities/build-order-by';
+import {
+  buildOrderBy,
+  buildOrderByForListings,
+} from '../../../src/utilities/build-order-by';
+
+describe('Testing buildOrderByForListings', () => {
+  it('should return applicationDueDate in array when orderBy contains applicationDueDate', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.applicationDates],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ applicationDueDate: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return marketingType in array when orderBy contains marketingType', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.marketingType],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ marketingType: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return closedAt in array when orderBy contains mostRecentlyClosed', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.mostRecentlyClosed],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ closedAt: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return publishedAt in array when orderBy contains mostRecentlyPublished', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.mostRecentlyPublished],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ publishedAt: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return updatedAt in array when orderBy contains mostRecentlyUpdated', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.mostRecentlyUpdated],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ updatedAt: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return name in array when orderBy contains name', () => {
+    expect(
+      buildOrderByForListings([ListingOrderByKeys.name], [OrderByEnum.ASC]),
+    ).toEqual([{ name: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return marketingType in array when orderBy contains status', () => {
+    expect(
+      buildOrderByForListings([ListingOrderByKeys.status], [OrderByEnum.ASC]),
+    ).toEqual([{ status: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return unitsAvailable in array when orderBy contains unitsAvailable', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.unitsAvailable],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ unitsAvailable: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return isWaitlistOpen in array when orderBy contains waitlistOpen', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.waitlistOpen],
+        [OrderByEnum.ASC],
+      ),
+    ).toEqual([{ isWaitlistOpen: 'asc' }, { name: 'asc' }]);
+  });
+
+  it('should return undefined when orderBy contains a value that is not an enum', () => {
+    expect(buildOrderByForListings(['order1'], [OrderByEnum.ASC])).toEqual([
+      undefined,
+      { name: 'asc' },
+    ]);
+  });
+
+  it('should return undefined when arrays are of unequal length', () => {
+    expect(
+      buildOrderByForListings(
+        [ListingOrderByKeys.name],
+        [OrderByEnum.ASC, OrderByEnum.ASC],
+      ),
+    ).toEqual(undefined);
+  });
+
+  it('should return undefined when both arrays are empty', () => {
+    expect(buildOrderByForListings([], [])).toEqual(undefined);
+  });
+});
+
 describe('Testing buildOrderBy', () => {
-  it('should return correctly mapped array when both arrays have length', () => {
+  it('should return correctly mapped array when both arrays have the same length', () => {
     expect(
       buildOrderBy(['order1', 'order2'], [OrderByEnum.ASC, OrderByEnum.DESC]),
     ).toEqual([{ order1: 'asc' }, { order2: 'desc' }]);
   });
+
   it('should return empty array when both arrays are empty', () => {
     expect(buildOrderBy([], [])).toEqual(undefined);
   });


Reviews:
user: login: emilyjablonski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T21:51:51Z
user: login: mcgarrye
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:01:00Z
user: login: emilyjablonski
body: Fabulous ✨ 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T21:12:08Z

Review comments:
diff_hunk: @@ -837,6 +840,9 @@ describe('Testing listing service', () => {
           {
             name: 'asc',
           },
+          {
path: api/test/unit/services/listing.service.spec.ts
user: login: emilyjablonski
body: **question:** J curious why this is duplicated?
created_at: 2025-04-23T21:51:51Z
author_association: COLLABORATOR
diff_hunk: @@ -837,6 +840,9 @@ describe('Testing listing service', () => {
           {
             name: 'asc',
           },
+          {
path: api/test/unit/services/listing.service.spec.ts
user: login: mcgarrye
body: The code doesn't check if it is already attempting to sort by name so it appends the second name sort regardless.
created_at: 2025-04-23T22:01:00Z
author_association: COLLABORATOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-23T20:03:13Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *partners-bloom-dev* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7235db0cf5624ebbc90af974f4188f30def07faf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/partners-bloom-dev/deploys/680a9793cc1f750008ed2813 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-4835--partners-bloom-dev.netlify.app](https://deploy-preview-4835--partners-bloom-dev.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ4MzUtLXBhcnRuZXJzLWJsb29tLWRldi5uZXRsaWZ5LmFwcCJ9.f_UxLIVpkJfxzWrRGv-j7PyuEHnGas0Lil90EecGc0I)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [partners-bloom-dev Preview](https://deploy-preview-4835--partners-bloom-dev.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/partners-bloom-dev/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-04-23T20:03:34Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *bloom-public-seeds* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7235db0cf5624ebbc90af974f4188f30def07faf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/bloom-public-seeds/deploys/680a9793991b490008dd9fae |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-4835--bloom-public-seeds.netlify.app](https://deploy-preview-4835--bloom-public-seeds.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ4MzUtLWJsb29tLXB1YmxpYy1zZWVkcy5uZXRsaWZ5LmFwcCJ9.5wts_oHHQIFRYNoPdZlSmQYbNatfe5Vc7D6hEgUQI2s)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [bloom-public-seeds Preview](https://deploy-preview-4835--bloom-public-seeds.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/bloom-public-seeds/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-04-23T20:04:03Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *bloom-exygy-dev* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7235db0cf5624ebbc90af974f4188f30def07faf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/bloom-exygy-dev/deploys/680a97932864660008b3ffc3 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-4835--bloom-exygy-dev.netlify.app](https://deploy-preview-4835--bloom-exygy-dev.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ4MzUtLWJsb29tLWV4eWd5LWRldi5uZXRsaWZ5LmFwcCJ9.pbceThqVi57pbb5lx0N3LRkFm0PuWcC7SVVU9wtU31g)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [bloom-exygy-dev Preview](https://deploy-preview-4835--bloom-exygy-dev.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/bloom-exygy-dev/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: emilyjablonski
created_at: 2025-04-23T21:59:03Z
body: Do you have a repeatable set of repro steps? I can't repro this on main, and want to make sure I can compare the same steps! I have 11 open listings locally.
author_association: COLLABORATOR
user: login: mcgarrye
created_at: 2025-04-23T22:02:08Z
body: > Do you have a repeatable set of repro steps? I can't repro this on main, and want to make sure I can compare the same steps! I have 11 open listings locally.

It's tricky to repo. The more listings with the same status the more likely it is to appear. Sometimes bring the app down and restarting it, and hard reloading the page will give a different result but no promises
author_association: COLLABORATOR
user: login: emilyjablonski
created_at: 2025-04-23T22:10:32Z
body: @mcgarrye I don't know that we'll be able to confidently know we've fixed it unless we get repeatable steps - otherwise it's just a best guess, which is fine if that's all we have! Is it possible there's a specific number of listings or a minimum number / default sorting that generates the issue?
author_association: COLLABORATOR
user: login: mcgarrye
created_at: 2025-04-24T15:17:54Z
body: > https://deploy-preview-4835--partners-bloom-dev.netlify.app

The issue is currently present on Core for open listings. One could download a copy of the core db locally and see if it resolves the issue
author_association: COLLABORATOR
user: login: emilyjablonski
created_at: 2025-04-28T16:10:40Z
body: Sounds good! @mcgarrye Can I confirm if that's a step you took in testing? It was looking good to me locally, but just want to confirm that's been done at least once :) Then lgtm!
author_association: COLLABORATOR
user: login: mcgarrye
created_at: 2025-04-28T19:27:14Z
body: > Sounds good! @mcgarrye Can I confirm if that's a step you took in testing? It was looking good to me locally, but just want to confirm that's been done at least once :) Then lgtm!

I sure did!
author_association: COLLABORATOR