Skip to content

Commit fe2c8f7

Browse files
mittistormengithub-actions[bot]lalmqvistsvjsonmomentiris
authored
Epic/uth 227 publicera alla bilplatser fran onecore (#196)
* add endpoint to get vacant parkingspaces * show vacant parkinspaces on a new page * create endpoint and method to create multiple listings. * create endpoint in core * add endpoint and logic to portal backend * refactor UI and publish vacant parkingspaces * handle potential empty PublishedTo-date for NON_SCORED listings * handle re-render errors * handle publishing of large batches * rename kötyp to Uthyrningsmetod * filter out offered as well as ready for offer listings from vacant parking spaces * disable creation of application for non scored listings * Don't render applicants or offers tabs on details page for NON_SCORED rental rules * refactor parking-spaces endpoints in core and leasing service to take body param list of rentalobjectIds and return rentalobjects. * parallelize vacant-parkingspace request * remove all xpand-sync code in internal-portal be and fe and core * remove xpand-internal-parkingspaces-sync from lease-service * Feat/uth 236 get all parking space listings from one core (#35) * Feat: UTH-236 Get All Parking Space Listings From OneCore Added missing functionality to filter result based on a ContactCode. Scored/internal Listings where the user doesn't have a housing contract is filtered out. * Feat:UTH-237 Get Published Lisings by RentalObjectCode (#42) To be able to show the parking space detail page on mimer.nu we need to be able to retrieve published listings by RentalObjectCode. * filter on RentalObjectCode has been added to GET /listings in both core and lesing * Feat:UTH-247-Rental-Object-Search * UTH-238: Unpublish listing (#38) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Linda Almqvist <linda.almqvist@prototyp.se> Co-authored-by: Sven Johansson <sven.johansson@iteam.se> Co-authored-by: Andreas Lundqvist <31646645+momentiris@users.noreply.github.com> Co-authored-by: Linda Almqvist <34321514+lalmqvist@users.noreply.github.com> Co-authored-by: lindblomdavid <65215239+lindblomdavid@users.noreply.github.com>
1 parent 878f61d commit fe2c8f7

File tree

93 files changed

+6309
-4229
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+6309
-4229
lines changed

.env.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@
77
SQLSERVER_IMAGE=mcr.microsoft.com/mssql/server:2022-latest
88
SQLSERVER_PLATFORM=linux/amd64
99
SQLSERVER_USER=root
10+
11+
# Alternative for M* Macs, in case azure-sql-edge does not work
12+
#SQLSERVER_IMAGE=mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-20.04
13+
14+
#DOCKER_SQL_PLATFORM=linux/amd64
15+
#DOCKER_SQL_USER=root

.github/workflows/build-and-push.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jobs:
5555
env:
5656
# Prefer workflow_run context if present, otherwise fall back defaults
5757
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch || (github.ref_type == 'branch' && github.ref_name) }}
58-
HEAD_TAG: ${{ github.event.workflow_run.head_branch == null && github.ref_type == 'tag' && github.ref_name || '' }}
59-
HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
58+
HEAD_TAG: ${{ github.event.workflow_run.head_branch == null && github.ref_type == 'tag' && github.ref_name || '' }}
59+
HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
6060
outputs:
6161
matrix: ${{ steps.read-matrix.outputs.matrix }}
6262
build_type: ${{ steps.build-type.outputs.build_type }}
@@ -145,7 +145,7 @@ jobs:
145145
run: |
146146
BRANCH_TAG="${{ steps.build-type.outputs.build_tag_base }}"
147147
148-
LATEST=$(crane ls "ghcr.io/bostads-ab-mimer/onecore/onecore-core" \
148+
LATEST=$(crane ls $IMAGE \
149149
| grep "^${BRANCH_TAG}\." \
150150
| sed "s/^${BRANCH_TAG}\.//" \
151151
| sort -n \
@@ -214,7 +214,7 @@ jobs:
214214
- name: Render build_args
215215
id: render-build-args
216216
env:
217-
BUILD_TYPE: ${{ needs.prepare.outputs.build_type }}
217+
BUILD_TYPE: ${{ needs.prepare.outputs.build_type }}
218218
RELEASE_BUILD_ARGS: ${{ (matrix.build_args.release && toJSON(matrix.build_args.release)) }}
219219
TEST_BUILD_ARGS: ${{ (matrix.build_args.test && toJSON(matrix.build_args.test)) }}
220220
run: |
@@ -286,4 +286,3 @@ jobs:
286286
git add $(git ls-files -m '*package*.json')
287287
git commit -m "chore: bump version to ${new_version} [skip ci]"
288288
git push --follow-tags
289-

apps/internal-portal/backend/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @type {import('jest').Config} */
2-
export default {
2+
module.exports = {
33
preset: 'ts-jest',
44
testEnvironment: 'node',
55
moduleNameMapper: {

apps/internal-portal/backend/src/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { routes as contactsRoutes } from './services/leasing-service/contacts'
55
import { routes as offersRoutes } from './services/leasing-service/offers'
66
import { routes as listingsRoutes } from './services/leasing-service/listings'
77
import { routes as invoicesRoutes } from './services/invoices/invoices'
8+
import { routes as rentalObjectsRoutes } from './services/leasing-service/rental-objects'
89

910
const router = new KoaRouter()
1011

1112
commentsRoutes(router)
1213
contactsRoutes(router)
1314
offersRoutes(router)
1415
listingsRoutes(router)
16+
rentalObjectsRoutes(router)
17+
1518
propertyInfoRoutes(router)
1619
invoicesRoutes(router)
1720

apps/internal-portal/backend/src/services/auth-service/adapters/msal.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import config from '../../../common/config'
21
import msal from '@azure/msal-node'
32
import axios from 'axios'
43
import {
@@ -8,10 +7,10 @@ import {
87
KoaContext,
98
} from './types'
109

11-
const redirectUri = config.auth.msal.redirectUri || config.msal.redirectUri
12-
const postLogoutRedirectUri =
13-
config.auth.msal.postLogoutRedirectUri || config.msal.postLogoutRedirectUri
10+
import config from '../../../common/config'
1411

12+
const redirectUri = config.msal.redirectUri
13+
const postLogoutRedirectUri = config.msal.postLogoutRedirectUri
1514
const cryptoProvider = new msal.CryptoProvider()
1615

1716
const defaultOptions: AuthOptions = {

apps/internal-portal/backend/src/services/leasing-service/adapters/core-adapter.ts

Lines changed: 134 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
CreateNoteOfInterestErrorCodes,
55
DetailedApplicant,
66
GetActiveOfferByListingIdErrorCodes,
7-
InternalParkingSpaceSyncSuccessResponse,
87
Listing,
98
ListingStatus,
109
Offer,
@@ -16,6 +15,7 @@ import {
1615
Comment,
1716
CommentThread,
1817
CommentThreadId,
18+
RentalObject,
1919
Invoice,
2020
Lease,
2121
} from '@onecore/types'
@@ -40,12 +40,20 @@ const getListingsWithApplicants = async (
4040
url: url,
4141
})
4242

43+
const listings = listingsResponse.data?.content
44+
45+
listings.sort((a, b) => {
46+
const dateA = new Date(a.publishedFrom).getTime()
47+
const dateB = new Date(b.publishedFrom).getTime()
48+
return dateB - dateA
49+
})
50+
4351
if (querystring !== 'type=offered') {
44-
return { ok: true, data: listingsResponse.data.content }
52+
return { ok: true, data: listings }
4553
}
4654

4755
const withOffers = await Promise.all(
48-
listingsResponse.data.content.map(async (listing) => {
56+
listings.map(async (listing) => {
4957
const offer = await getActiveOfferByListingId(listing.id)
5058
if (!offer.ok) {
5159
throw new Error('Failed to get offer')
@@ -292,19 +300,42 @@ const createOffer = async (params: {
292300
}
293301
}
294302

295-
const syncInternalParkingSpacesFromXpand = async (): Promise<
296-
AdapterResult<InternalParkingSpaceSyncSuccessResponse, 'unknown'>
297-
> => {
303+
const createListings = async (
304+
listingData: Omit<Listing, 'id' | 'rentalObject'>
305+
): Promise<AdapterResult<Listing, 'conflict' | 'unknown'>> => {
298306
try {
299-
const response = await getFromCore<{
300-
content: InternalParkingSpaceSyncSuccessResponse
301-
}>({
307+
const response = await getFromCore<{ content: Listing }>({
302308
method: 'post',
303-
url: `${coreBaseUrl}/listings/sync-internal-from-xpand`,
309+
url: `${coreBaseUrl}/listings`,
310+
data: listingData,
304311
})
305312

306313
return { ok: true, data: response.data.content }
307-
} catch {
314+
} catch (err) {
315+
const axiosError = err as AxiosError
316+
if (axiosError.response?.status === HttpStatusCode.Conflict) {
317+
return { ok: false, err: 'conflict', statusCode: 409 }
318+
}
319+
return { ok: false, err: 'unknown', statusCode: 500 }
320+
}
321+
}
322+
323+
const createMultipleListings = async (
324+
listingsData: Array<Omit<Listing, 'id' | 'rentalObject'>>
325+
): Promise<AdapterResult<Array<Listing>, 'partial-failure' | 'unknown'>> => {
326+
try {
327+
const response = await getFromCore<{ content: Array<Listing> }>({
328+
method: 'post',
329+
url: `${coreBaseUrl}/listings/batch`,
330+
data: { listings: listingsData },
331+
})
332+
333+
return { ok: true, data: response.data.content }
334+
} catch (err) {
335+
const axiosError = err as AxiosError
336+
if (axiosError.response?.status === 207) {
337+
return { ok: false, err: 'partial-failure', statusCode: 207 }
338+
}
308339
return { ok: false, err: 'unknown', statusCode: 500 }
309340
}
310341
}
@@ -313,9 +344,7 @@ const deleteListing = async (
313344
listingId: number
314345
): Promise<AdapterResult<null, 'conflict' | 'unknown'>> => {
315346
try {
316-
await getFromCore<{
317-
content: InternalParkingSpaceSyncSuccessResponse
318-
}>({
347+
await getFromCore({
319348
method: 'delete',
320349
url: `${coreBaseUrl}/listings/${listingId}`,
321350
})
@@ -348,6 +377,22 @@ const closeListing = async (
348377
}
349378
}
350379

380+
const expireListing = async (
381+
listingId: number
382+
): Promise<AdapterResult<null, 'unknown'>> => {
383+
try {
384+
await getFromCore({
385+
method: 'put',
386+
url: `${coreBaseUrl}/listings/${listingId}/status`,
387+
data: { status: ListingStatus.Expired },
388+
})
389+
390+
return { ok: true, data: null }
391+
} catch {
392+
return { ok: false, err: 'unknown', statusCode: 500 }
393+
}
394+
}
395+
351396
const acceptOffer = async (
352397
offerId: string
353398
): Promise<AdapterResult<Array<Listing>, ReplyToOfferErrorCodes>> => {
@@ -565,6 +610,23 @@ const removeComment = async (
565610
}
566611
}
567612

613+
const getVacantParkingSpaces = async (): Promise<
614+
AdapterResult<RentalObject[], unknown>
615+
> => {
616+
try {
617+
const response = await getFromCore<{ content: RentalObject[] }>({
618+
method: 'get',
619+
url: `${coreBaseUrl}/vacant-parkingspaces`,
620+
})
621+
return { ok: true, data: response.data.content }
622+
} catch (e) {
623+
if (e instanceof AxiosError && e.response?.status === 401) {
624+
return { ok: false, err: 'unauthorized', statusCode: 401 }
625+
}
626+
return { ok: false, err: 'unknown', statusCode: 500 }
627+
}
628+
}
629+
568630
async function getInvoicesByContactCode(
569631
contactCode: string
570632
): Promise<AdapterResult<Invoice[], 'not-found' | 'unknown'>> {
@@ -603,6 +665,59 @@ async function getLeasesByContactCode(contactCode: string) {
603665
return { ok: true, data: response.data.content }
604666
}
605667

668+
const createLeaseForNonScoredParkingSpace = async (params: {
669+
parkingSpaceId: string
670+
contactCode: string
671+
}): Promise<
672+
AdapterResult<
673+
unknown,
674+
| 'internal-credit-check-failed'
675+
| 'external-credit-check-failed'
676+
| 'invalid-address'
677+
| 'already-has-lease'
678+
| 'unknown'
679+
>
680+
> => {
681+
try {
682+
const response = await getFromCore<any>({
683+
method: 'post',
684+
url: `${coreBaseUrl}/parking-spaces/${params.parkingSpaceId}/leases`,
685+
data: { contactCode: params.contactCode },
686+
})
687+
688+
return { ok: true, data: response.data.content }
689+
} catch (err) {
690+
if (err instanceof AxiosError && err.response?.data) {
691+
const statusCode = err.response.status
692+
// Check error at root level first, then nested in content
693+
const errorCode =
694+
err.response.data?.error || err.response.data?.content?.errorCode
695+
696+
// Handle both 400 BadRequest and 404 NotFound for validation errors
697+
if (statusCode === HttpStatusCode.BadRequest || statusCode === 404) {
698+
// Map error codes from core service
699+
if (errorCode === 'internal-credit-check-failed') {
700+
return { ok: false, err: 'internal-credit-check-failed', statusCode }
701+
}
702+
if (errorCode === 'external-credit-check-failed') {
703+
return { ok: false, err: 'external-credit-check-failed', statusCode }
704+
}
705+
if (
706+
errorCode === 'invalid-address' ||
707+
errorCode === 'applicant-missing-address'
708+
) {
709+
return { ok: false, err: 'invalid-address', statusCode }
710+
}
711+
if (errorCode === 'already-has-lease') {
712+
return { ok: false, err: 'already-has-lease', statusCode }
713+
}
714+
}
715+
}
716+
717+
return { ok: false, err: 'unknown', statusCode: 500 }
718+
}
719+
}
720+
606721
export {
607722
addComment,
608723
removeComment,
@@ -614,18 +729,22 @@ export {
614729
getTenantByContactCode,
615730
getContactByContactCode,
616731
getContactByNationalRegistrationNumber,
732+
createListings,
733+
createMultipleListings,
617734
createNoteOfInterestForInternalParkingSpace,
618735
validatePropertyRentalRules,
619736
validateResidentialAreaRentalRules,
620-
syncInternalParkingSpacesFromXpand,
621737
createOffer,
622738
deleteListing,
623739
closeListing,
740+
expireListing,
624741
acceptOffer,
625742
denyOffer,
626743
getActiveOfferByListingId,
627744
getApplicationProfileByContactCode,
628745
createOrUpdateApplicationProfile,
746+
getVacantParkingSpaces,
629747
getInvoicesByContactCode,
630748
getLeasesByContactCode,
749+
createLeaseForNonScoredParkingSpace,
631750
}

0 commit comments

Comments
 (0)