Repository: UK-Export-Finance/tfs-api
Pull request #976: feat(GIFT-10023): create a facility (repayment profiles)
Author: login: tony-barnes-ukef
Labels: name: Type: chore
name: Type: Docs
name: Type: Feature
Created: 2025-04-11T15:22:12Z | Merged: 2025-04-15T08:32:32Z

## Introduction :pencil2:

This PR updates the "create facility" endpoint to consume and send repayment profiles to the GIFT API.

⚠️ Note: The following will be added in the next/future PRs:
- Extended API tests for repayment profile assertions.
- Improved `dueDate` validation (each due date should be unique).
- Improved `name` validation (each name should be unique).

## Resolution :heavy_check_mark:

- Update GIFT constants.
- Update facility creation DTO (`GiftFacilityCreationDto`).
- Create new DTOs:
  - `GiftRepaymentProfileAllocationDto`
  - `GiftRepaymentProfileDto`
- Create new repayment profile service, `GiftRepaymentProfileService`.
- Update/add appropriate unit tests.
- Update `GIftService.createFacility` to:
  - Call `giftRepaymentProfileService.createMany`
  - Return all validation errors from counterparties and repayment profile services.
  - Return repayment profiles in the response.
- Create new helper function: `mapAllValidationErrorResponses`.
- Update API tests.

## Miscellaneous :heavy_plus_sign:

- Add some missing documentation.
- Split up facility service unit tests.
- Create DRY `GiftValidationError` and `ValidationErrorResponse` types.
- Update all GIFT API tests to use `HttpStatus`.

## Payload/response examples - success

<details>
  <summary>Show/hide</summary>

### Successful payload
```json
{
    "overview": {
        "facilityId": "0022222088",
        "currency": "USD",
        "name": "mock name",
        "facilityAmount": 600,
        "drawnAmount": 8888,
        "effectiveDate": "2023-01-01",
        "expiryDate": "2025-12-31",
        "endOfCoverDate": "2025-12-31",
        "dealId": "0026556789",
        "isRevolving": false,
        "productType": "Mock product name",
        "obligorUrn": "12345678"
    },
    "counterparties": [
        {
            "counterpartyUrn": "12345678",
            "roleId": "buyer",
            "startDate": "2025-04-10", 
            "exitDate": "2025-08-10",
            "sharePercentage": 20
        }
    ],
    "repaymentProfiles": [
        {
          "name": "Mock repayment profile 1",
          "allocations": [
            {
              "amount": 100,
              "dueDate": "2025-01-02"
            },
            {
              "amount": 50,
              "dueDate": "2025-01-03"
            }
          ]
        },
        {
          "name": "Mock repayment profile 2",
          "allocations": [
            {
              "amount": 20,
              "dueDate": "2025-01-03"
            },
            {
              "amount": 30,
              "dueDate": "2025-01-04"
            }
          ]
        }
    ]
}
```

### Successful response body

```json
{
    "facilityId": "0022222088",
    "streamId": "b884de74-f7ab-4a13-9891-5f44b244b9da",
    "streamVersion": 1,
    "name": "mock name",
    "obligorUrn": "12345678",
    "currency": "USD",
    "facilityAmount": 600,
    "drawnAmount": 0,
    "availableAmount": 600,
    "effectiveDate": "2023-01-01",
    "expiryDate": "2025-12-31",
    "endOfCoverDate": "2025-12-31",
    "dealId": "0026556789",
    "isRevolving": false,
    "isDraft": true,
    "createdDatetime": "2025-04-11T15:12:56.903Z",
    "productType": "Export Insurance Policy (EXIP)",
    "workPackageId": 7002,
    "counterparties": [
        {
            "id": 7003,
            "eventType": "AddCounterparty",
            "eventDate": "2025-04-08",
            "eventData": {
                "counterpartyUrn": "12345678",
                "roleId": "buyer",
                "startDate": "2025-04-10",
                "exitDate": "2025-08-10",
                "sharePercentage": 20
            },
            "isApproved": false
        }
    ],
    "repaymentProfiles": [
        {
            "id": 7005,
            "eventType": "AddManualRepaymentProfile",
            "eventDate": "2025-04-08",
            "eventData": {
                "name": "Mock repayment profile 1",
                "allocations": [
                    {
                        "dueDate": "2025-01-02",
                        "amount": 100
                    },
                    {
                        "dueDate": "2025-01-03",
                        "amount": 50
                    }
                ]
            },
            "isApproved": false
        },
        {
            "id": 7006,
            "eventType": "AddManualRepaymentProfile",
            "eventDate": "2025-04-08",
            "eventData": {
                "name": "Mock repayment profile 2",
                "allocations": [
                    {
                        "dueDate": "2025-01-03",
                        "amount": 20
                    },
                    {
                        "dueDate": "2025-01-04",
                        "amount": 30
                    }
                ]
            },
            "isApproved": false
        }
    ]
}
```

</details>


## Payload/response examples - validation errors
<details>
  <summary>Show/hide</summary>

### Validation errors payload - example 1 - TFS validation errors

```json
{
    "message": [
        "repaymentProfiles.0.allocations.0.amount should not be null or undefined",
        "repaymentProfiles.0.allocations.0.amount must not be less than 0",
        "repaymentProfiles.0.allocations.0.amount must be a number conforming to the specified constraints",
        "repaymentProfiles.0.allocations.0.dueDate should not be null or undefined",
        "repaymentProfiles.0.allocations.0.dueDate must be longer than or equal to 10 characters",
        "repaymentProfiles.0.allocations.0.dueDate must be a string",
        "repaymentProfiles.0.allocations.1.amount should not be null or undefined",
        "repaymentProfiles.0.allocations.1.amount must not be less than 0",
        "repaymentProfiles.0.allocations.1.amount must be a number conforming to the specified constraints",
        "repaymentProfiles.0.allocations.1.dueDate should not be null or undefined",
        "repaymentProfiles.0.allocations.1.dueDate must be longer than or equal to 10 characters",
        "repaymentProfiles.0.allocations.1.dueDate must be a string",
        "repaymentProfiles.1.allocations.0.amount should not be null or undefined",
        "repaymentProfiles.1.allocations.0.amount must not be less than 0",
        "repaymentProfiles.1.allocations.0.amount must be a number conforming to the specified constraints",
        "repaymentProfiles.1.allocations.0.dueDate should not be null or undefined",
        "repaymentProfiles.1.allocations.0.dueDate must be longer than or equal to 10 characters",
        "repaymentProfiles.1.allocations.0.dueDate must be a string"
    ],
    "error": "Bad Request",
    "statusCode": 400
}
```

### Validation errors payload - example 2 - GIFT API response

```json
{
    "statusCode": 400,
    "message": "Validation errors with facility entity(s)",
    "validationErrors": [
        {
            "entityName": "Repayment profile",
            "index": 0,
            "message": "Validation error: [{\"path\":[\"allocations\"],\"message\":\"Repayment allocation dates must be within facility active period (1 January 2023 to 31 December 2025 inclusive)\"},{\"path\":[\"allocations\"],\"message\":\"The sum of all repayment allocation amounts (USD 300,000,000,000.00) must be less than or equal to the facility amount (USD 600.00)\"}]",
            "status": 400,
            "type": "api-error-response",
            "validationErrors": [
                {
                    "path": [
                        "allocations"
                    ],
                    "message": "Repayment allocation dates must be within facility active period (1 January 2023 to 31 December 2025 inclusive)"
                },
                {
                    "path": [
                        "allocations"
                    ],
                    "message": "The sum of all repayment allocation amounts (USD 300,000,000,000.00) must be less than or equal to the facility amount (USD 600.00)"
                }
            ]
        },
        {
            "entityName": "Repayment profile",
            "index": 1,
            "message": "Validation error: [{\"path\":[\"allocations\"],\"message\":\"The sum of all repayment allocation amounts (USD 700,000,000,000.00) must be less than or equal to the facility amount (USD 600.00)\"}]",
            "status": 400,
            "type": "api-error-response",
            "validationErrors": [
                {
                    "path": [
                        "allocations"
                    ],
                    "message": "The sum of all repayment allocation amounts (USD 700,000,000,000.00) must be less than or equal to the facility amount (USD 600.00)"
                }
            ]
        }
    ]
}
```

</details>




Diff:
diff --git a/src/constants/examples/gift.examples.constant.ts b/src/constants/examples/gift.examples.constant.ts
index 5e1e03e5..5a6c59a6 100644
--- a/src/constants/examples/gift.examples.constant.ts
+++ b/src/constants/examples/gift.examples.constant.ts
@@ -14,6 +14,10 @@ const FACILITY_ID: UkefId = '0030000321';
 
 const WORK_PACKAGE_ID = 123;
 
+/**
+ * Example counterparty.
+ * NOTE: Each counterparty URN is unique.
+ */
 const COUNTERPARTY = () => ({
   counterpartyUrn: chance.string({
     length: VALIDATION.COUNTERPARTY.COUNTERPARTY_URN.MAX_LENGTH,
@@ -25,6 +29,31 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
+  const today = new Date();
+
+  const day = '01';
+  const month = '02';
+  const year = today.getFullYear() + index;
+
+  const dueDate = `${day}-${month}-${year}`;
+
+  return {
+    amount: 5000,
+    dueDate,
+  };
+};
+
+const REPAYMENT_PROFILE = () => ({
+  name: chance.string(),
+  allocations: [REPAYMENT_PROFILE_ALLOCATION(0), REPAYMENT_PROFILE_ALLOCATION(1)],
+});
+
 const FACILITY_OVERVIEW = {
   facilityId: FACILITY_ID,
   streamId: '7d915bfa-0069-4aaa-92c5-013925f019a1',
@@ -32,7 +61,7 @@ const FACILITY_OVERVIEW = {
   name: 'Amazing facility',
   obligorUrn: '01234567',
   currency: CURRENCIES.USD,
-  facilityAmount: 1000000,
+  facilityAmount: 10000,
   effectiveDate: '2025-01-01',
   expiryDate: '2025-02-01',
   endOfCoverDate: '2025-03-01',
@@ -50,6 +79,7 @@ const FACILITY_OVERVIEW = {
 const FACILITY_CREATION_PAYLOAD: GiftFacilityCreationDto = {
   overview: FACILITY_OVERVIEW,
   counterparties: [COUNTERPARTY(), COUNTERPARTY()],
+  repaymentProfiles: [REPAYMENT_PROFILE(), REPAYMENT_PROFILE()],
 
   /**
    * NOTE: the below properties are purely for example purposes.
@@ -57,7 +87,6 @@ const FACILITY_CREATION_PAYLOAD: GiftFacilityCreationDto = {
    */
   fees: [],
   obligations: [],
-  repaymentProfiles: [],
 };
 
 /**
@@ -78,5 +107,7 @@ export const GIFT_EXAMPLES = {
   FACILITY_ID,
   FACILITY_OVERVIEW,
   FACILITY_RESPONSE_DATA,
+  REPAYMENT_PROFILE,
+  REPAYMENT_PROFILE_ALLOCATION,
   WORK_PACKAGE_ID,
 };
diff --git a/src/constants/gift.constant.ts b/src/constants/gift.constant.ts
index 29175fd9..ae38be9f 100644
--- a/src/constants/gift.constant.ts
+++ b/src/constants/gift.constant.ts
@@ -18,12 +18,15 @@ export const GIFT = {
   },
   ENTITY_NAMES: {
     COUNTERPARTY: 'Counterparty',
+    REPAYMENT_PROFILE: 'Repayment profile',
   },
   PATH: {
     FACILITY: '/facility',
-    WORK_PACKAGE: '/work-package',
     COUNTERPARTY: '/counterparty',
     CREATION_EVENT: '/creation-event',
+    MANUAL: '/manual',
+    REPAYMENT_PROFILE: '/repayment-profile',
+    WORK_PACKAGE: '/work-package',
   },
   VALIDATION: {
     FACILITY: {
@@ -47,5 +50,12 @@ export const GIFT = {
       SHARE_PERCENTAGE: { MIN: 0.1, MAX: 100 },
       START_DATE: DATE_STRING_VALIDATION,
     },
+    REPAYMENT_PROFILE: {
+      NAME: { MIN_LENGTH: 0, MAX_LENGTH: 120 },
+      ALLOCATION: {
+        DUE_DATE: DATE_STRING_VALIDATION,
+        AMOUNT: { MIN: 0 },
+      },
+    },
   },
 };
diff --git a/src/modules/gift/dto/facility-creation.ts b/src/modules/gift/dto/facility-creation.ts
index d4518c95..9c7106a7 100644
--- a/src/modules/gift/dto/facility-creation.ts
+++ b/src/modules/gift/dto/facility-creation.ts
@@ -5,9 +5,10 @@ import { ArrayNotEmpty, IsArray, IsDefined, IsNotEmptyObject, ValidateNested } f
 
 import { GiftFacilityDto } from './facility';
 import { GiftFacilityCounterpartyDto } from './facility-counterparty';
+import { GiftRepaymentProfileDto } from './repayment-profile';
 
 const {
-  GIFT: { COUNTERPARTY, FACILITY_OVERVIEW },
+  GIFT: { COUNTERPARTY, FACILITY_OVERVIEW, REPAYMENT_PROFILE },
 } = EXAMPLES;
 
 /**
@@ -39,14 +40,24 @@ export class GiftFacilityCreationDto {
   @ValidateNested()
   counterparties: GiftFacilityCounterpartyDto[];
 
+  @ApiProperty({
+    isArray: true,
+    example: [REPAYMENT_PROFILE(), REPAYMENT_PROFILE()],
+    required: true,
+  })
+  @IsArray()
+  @ArrayNotEmpty()
+  @IsDefined()
+  @Type(() => GiftRepaymentProfileDto)
+  @ValidateNested()
+  repaymentProfiles: GiftRepaymentProfileDto[];
+
   /**
    * NOTE: the below properties are purely for example/context purposes.
    * These will be populated in upcoming PRs:
-   * GIFT-10023
    * GIFT-10025
    * GIFT-10026
    */
   fees: [];
   obligations: [];
-  repaymentProfiles: [];
 }
diff --git a/src/modules/gift/dto/index.ts b/src/modules/gift/dto/index.ts
index 691641a0..26348fe1 100644
--- a/src/modules/gift/dto/index.ts
+++ b/src/modules/gift/dto/index.ts
@@ -4,3 +4,4 @@ export * from './facility-creation';
 export * from './facility-get';
 export * from './facility-post-gift-response';
 export * from './get-facility-params.dto';
+export * from './repayment-profile';
diff --git a/src/modules/gift/dto/repayment-profile-allocation.ts b/src/modules/gift/dto/repayment-profile-allocation.ts
new file mode 100644
index 00000000..34bdcb95
--- /dev/null
+++ b/src/modules/gift/dto/repayment-profile-allocation.ts
@@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { IsDefined, IsNumber, IsString, Length, Min } from 'class-validator';
+
+const {
+  GIFT: { REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: {
+    REPAYMENT_PROFILE: { ALLOCATION: VALIDATION },
+  },
+} = GIFT;
+
+/**
+ * GIFT repayment profile allocation DTO.
+ * These fields are required for APIM to create a repayment profile allocation in GIFT.
+ */
+export class GiftRepaymentProfileAllocationDto {
+  @IsDefined()
+  @IsNumber()
+  @Min(VALIDATION.AMOUNT.MIN)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().amount,
+  })
+  amount: number;
+
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.DUE_DATE.MIN_LENGTH, VALIDATION.DUE_DATE.MAX_LENGTH)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().dueDate,
+  })
+  dueDate: string;
+}
diff --git a/src/modules/gift/dto/repayment-profile.ts b/src/modules/gift/dto/repayment-profile.ts
new file mode 100644
index 00000000..08b0c74e
--- /dev/null
+++ b/src/modules/gift/dto/repayment-profile.ts
@@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE().name,
+  })
+  name: string;
+
+  @IsArray()
+  @ArrayNotEmpty()
+  @IsDefined()
+  @Type(() => GiftRepaymentProfileAllocationDto)
+  @ValidateNested()
+  @ApiProperty({
+    isArray: true,
+    example: [REPAYMENT_PROFILE_ALLOCATION(), REPAYMENT_PROFILE_ALLOCATION()],
+    required: true,
+    type: GiftRepaymentProfileAllocationDto,
+  })
+  allocations: GiftRepaymentProfileAllocationDto[];
+}
diff --git a/src/modules/gift/gift.controller.test.ts b/src/modules/gift/gift.controller.test.ts
index c09c8073..5aa7368e 100644
--- a/src/modules/gift/gift.controller.test.ts
+++ b/src/modules/gift/gift.controller.test.ts
@@ -3,6 +3,7 @@ import { mockResponse200, mockResponse201 } from '@ukef-test/http-response';
 
 import { GiftController } from './gift.controller';
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
 import { GiftHttpService } from './gift-http.service';
 
@@ -16,6 +17,7 @@ const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
 describe('GiftController', () => {
   let giftHttpService: GiftHttpService;
   let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
   let giftService: GiftService;
   let controller: GiftController;
 
@@ -30,7 +32,9 @@ describe('GiftController', () => {
     giftHttpService = new GiftHttpService();
 
     counterpartyService = new GiftCounterpartyService(giftHttpService);
-    giftService = new GiftService(giftHttpService, counterpartyService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    giftService = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
 
     mockResSend = jest.fn();
 
diff --git a/src/modules/gift/gift.counterparty.service.ts b/src/modules/gift/gift.counterparty.service.ts
index e222afc1..64e4a53e 100644
--- a/src/modules/gift/gift.counterparty.service.ts
+++ b/src/modules/gift/gift.counterparty.service.ts
@@ -20,6 +20,7 @@ export class GiftCounterpartyService {
   /**
    * Create a GIFT counterparty
    * @param {GiftFacilityCounterpartyDto} payload: Counterparty data
+   * @param {Number} workPackageId: Facility work package ID
    * @returns {Promise<AxiosResponse>}
    * @throws {Error}
    */
@@ -41,6 +42,7 @@ export class GiftCounterpartyService {
   /**
    * Create multiple GIFT counterparties
    * @param {Array<GiftFacilityCounterpartyDto>} counterpartiesData: Counterparties data
+   * @param {Number} workPackageId: Facility work package ID
    * @returns {Promise<Array<AxiosResponse>>}
    * @throws {Error}
    */
diff --git a/src/modules/gift/gift.module.ts b/src/modules/gift/gift.module.ts
index a176638a..5848aa9f 100644
--- a/src/modules/gift/gift.module.ts
+++ b/src/modules/gift/gift.module.ts
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
 
 import { GiftController } from './gift.controller';
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
 import { GiftHttpService } from './gift-http.service';
 
 @Module({
-  providers: [GiftHttpService, GiftService, GiftCounterpartyService],
+  providers: [GiftHttpService, GiftService, GiftCounterpartyService, GiftRepaymentProfileService],
   controllers: [GiftController],
 })
 export class GiftModule {}
diff --git a/src/modules/gift/gift.repayment-profile.service.test.ts b/src/modules/gift/gift.repayment-profile.service.test.ts
new file mode 100644
index 00000000..89e50bad
--- /dev/null
+++ b/src/modules/gift/gift.repayment-profile.service.test.ts
@@ -0,0 +1,144 @@
+import { HttpService } from '@nestjs/axios';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201, mockResponse500 } from '@ukef-test/http-response';
+
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+const {
+  GIFT: { REPAYMENT_PROFILE, WORK_PACKAGE_ID: mockWorkPackageId },
+} = EXAMPLES;
+
+const { PATH } = GIFT;
+
+const expectedPath = `${PATH.WORK_PACKAGE}/${mockWorkPackageId}${PATH.REPAYMENT_PROFILE}${PATH.MANUAL}${PATH.CREATION_EVENT}`;
+
+describe('GiftRepaymentProfileService', () => {
+  let httpService: HttpService;
+  let service: GiftRepaymentProfileService;
+
+  let giftHttpService;
+  let mockCreateOneResponse;
+  let mockHttpServicePost: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockCreateOneResponse = mockResponse201(REPAYMENT_PROFILE());
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockCreateOneResponse);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    service = new GiftRepaymentProfileService(giftHttpService);
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('createOne', () => {
+    const mockPayload = REPAYMENT_PROFILE();
+
+    it('should call giftHttpService.post', async () => {
+      await service.createOne(mockPayload, mockWorkPackageId);
+
+      expect(mockHttpServicePost).toHaveBeenCalledTimes(1);
+
+      expect(mockHttpServicePost).toHaveBeenCalledWith({
+        path: expectedPath,
+        payload: mockPayload,
+      });
+    });
+
+    describe('when giftHttpService.post is successful', () => {
+      it('should return the response of giftHttpService.post', async () => {
+        const response = await service.createOne(mockPayload, mockWorkPackageId);
+
+        expect(response).toEqual(mockCreateOneResponse);
+      });
+    });
+
+    describe('when giftHttpService.post returns an error', () => {
+      beforeEach(() => {
+        mockHttpServicePost = jest.fn().mockRejectedValueOnce(mockResponse500());
+
+        giftHttpService.post = mockHttpServicePost;
+
+        service = new GiftRepaymentProfileService(giftHttpService);
+      });
+
+      it('should thrown an error', async () => {
+        const promise = service.createOne(mockPayload, mockWorkPackageId);
+
+        const expected = 'Error creating repayment profile';
+
+        await expect(promise).rejects.toThrow(expected);
+      });
+    });
+  });
+
+  describe('createMany', () => {
+    const repaymentProfilesLength = 3;
+
+    const mockRepaymentProfiles = Array(repaymentProfilesLength).fill(REPAYMENT_PROFILE());
+
+    const mockPayload = mockRepaymentProfiles;
+
+    let mockCreateOne = jest.fn().mockResolvedValue(mockResponse201(mockRepaymentProfiles));
+
+    beforeEach(() => {
+      mockCreateOneResponse = mockResponse201(mockRepaymentProfiles);
+
+      giftHttpService.post = jest
+        .fn()
+        .mockResolvedValueOnce(mockResponse201(mockRepaymentProfiles[0]))
+        .mockResolvedValueOnce(mockResponse201(mockRepaymentProfiles[1]))
+        .mockResolvedValueOnce(mockResponse201(mockRepaymentProfiles[2]));
+
+      service = new GiftRepaymentProfileService(giftHttpService);
+
+      service.createOne = mockCreateOne;
+    });
+
+    it('should call service.createOne for each provided repayment profile', async () => {
+      await service.createMany(mockPayload, mockWorkPackageId);
+
+      expect(mockCreateOne).toHaveBeenCalledTimes(repaymentProfilesLength);
+
+      expect(mockCreateOne).toHaveBeenCalledWith(mockPayload[0], mockWorkPackageId);
+      expect(mockCreateOne).toHaveBeenCalledWith(mockPayload[1], mockWorkPackageId);
+      expect(mockCreateOne).toHaveBeenCalledWith(mockPayload[2], mockWorkPackageId);
+    });
+
+    describe('when service.createOne is successful', () => {
+      it('should return the response of multiple calls to service.createOne', async () => {
+        const response = await service.createMany(mockPayload, mockWorkPackageId);
+
+        const expected = [mockCreateOneResponse, mockCreateOneResponse, mockCreateOneResponse];
+
+        expect(response).toEqual(expected);
+      });
+    });
+
+    describe('when service.createOne returns an error', () => {
+      beforeEach(() => {
+        mockCreateOne = jest.fn().mockRejectedValueOnce(mockResponse500());
+
+        service = new GiftRepaymentProfileService(giftHttpService);
+
+        service.createOne = mockCreateOne;
+      });
+
+      it('should thrown an error', async () => {
+        const promise = service.createMany(mockPayload, mockWorkPackageId);
+
+        const expected = 'Error creating repayment profiles';
+
+        await expect(promise).rejects.toThrow(expected);
+      });
+    });
+  });
+});
diff --git a/src/modules/gift/gift.repayment-profile.service.ts b/src/modules/gift/gift.repayment-profile.service.ts
new file mode 100644
index 00000000..12c217e6
--- /dev/null
+++ b/src/modules/gift/gift.repayment-profile.service.ts
@@ -0,0 +1,60 @@
+import { Injectable } from '@nestjs/common';
+import { GIFT } from '@ukef/constants';
+import { AxiosResponse } from 'axios';
+
+import { GiftRepaymentProfileDto } from './dto';
+import { GiftHttpService } from './gift-http.service';
+
+const { PATH } = GIFT;
+
+/**
+ * GIFT repayment profile service.
+ * This is responsible for all repayment profile operations that call the GIFT API.
+ */
+@Injectable()
+export class GiftRepaymentProfileService {
+  constructor(private readonly giftHttpService: GiftHttpService) {
+    this.giftHttpService = giftHttpService;
+  }
+
+  /**
+   * Create a GIFT repayment profile
+   * @param {GiftRepaymentProfileDto} payload: Repayment profile data
+   * @param {Number} workPackageId: Facility work package ID
+   * @returns {Promise<AxiosResponse>}
+   * @throws {Error}
+   */
+  async createOne(payload: GiftRepaymentProfileDto, workPackageId: number): Promise<AxiosResponse> {
+    try {
+      const response = await this.giftHttpService.post<GiftRepaymentProfileDto>({
+        path: `${PATH.WORK_PACKAGE}/${workPackageId}${PATH.REPAYMENT_PROFILE}${PATH.MANUAL}${PATH.CREATION_EVENT}`,
+        payload,
+      });
+
+      return response;
+    } catch (error) {
+      console.error('Error creating repayment profile %o', error);
+
+      throw new Error('Error creating repayment profile', error);
+    }
+  }
+
+  /**
+   * Create multiple GIFT repayment profiles
+   * @param {Array<GiftRepaymentProfileDto>} repaymentProfilesData: Repayment profiles data
+   * @param {Number} workPackageId: Facility work package ID
+   * @returns {Promise<Array<AxiosResponse>>}
+   * @throws {Error}
+   */
+  async createMany(repaymentProfilesData: GiftRepaymentProfileDto[], workPackageId: number): Promise<Array<AxiosResponse>> {
+    try {
+      const responses = await Promise.all(repaymentProfilesData.map((repaymentProfile) => this.createOne(repaymentProfile, workPackageId)));
+
+      return responses;
+    } catch (error) {
+      console.error('Error creating repayment profiles %o', error);
+
+      throw new Error('Error creating repayment profiles', error);
+    }
+  }
+}
diff --git a/src/modules/gift/gift.service-createFacility-bad-requests.test.ts b/src/modules/gift/gift.service-createFacility-bad-requests.test.ts
new file mode 100644
index 00000000..4badb41e
--- /dev/null
+++ b/src/modules/gift/gift.service-createFacility-bad-requests.test.ts
@@ -0,0 +1,305 @@
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockCreateInitialFacilityResponse = mockResponse201(FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - bad requests', () => {
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockCreateInitialFacilityResponse);
+    createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
+
+    counterpartyService.createMany = createCounterpartiesSpy;
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+    service.createInitialFacility = createInitialFacilitySpy;
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe(`when a service returns a first item with a status that is ${HttpStatus.CREATED}, but other items have ${HttpStatus.BAD_REQUEST}`, () => {
+    const mockResponse = [
+      {
+        status: HttpStatus.CREATED,
+        data: {},
+      },
+      {
+        status: HttpStatus.BAD_REQUEST,
+        data: {
+          validationErrors: [{ message: 'mock validation error' }],
+        },
+      },
+    ] as AxiosResponse[];
+
+    describe('counterpartyService.createMany', () => {
+      beforeEach(() => {
+        createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        counterpartyService.createMany = createCounterpartiesSpy;
+      });
+
+      it('should return an object with mapped counterparty errors', async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.COUNTERPARTY,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.BAD_REQUEST,
+          data: {
+            statusCode: HttpStatus.BAD_REQUEST,
+            message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+
+    describe('repaymentProfiles.createMany', () => {
+      beforeEach(() => {
+        createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        repaymentProfileService.createMany = createRepaymentProfilesSpy;
+      });
+
+      it('should return an object with mapped counterparty errors', async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.BAD_REQUEST,
+          data: {
+            statusCode: HttpStatus.BAD_REQUEST,
+            message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+  });
+
+  describe(`when multiple services return items with ${HttpStatus.BAD_REQUEST} statuses`, () => {
+    const mockResponse = [
+      {
+        status: HttpStatus.BAD_REQUEST,
+        data: {
+          validationErrors: [{ message: 'mock validation error' }],
+        },
+      },
+      {
+        status: HttpStatus.BAD_REQUEST,
+        data: {
+          validationErrors: [{ message: 'mock validation error' }],
+        },
+      },
+    ] as AxiosResponse[];
+
+    beforeEach(() => {
+      createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+      createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+      counterpartyService.createMany = createCounterpartiesSpy;
+      repaymentProfileService.createMany = createRepaymentProfilesSpy;
+    });
+
+    it('should return an object with mapped errors for all service responses', async () => {
+      const response = await service.createFacility(mockPayload);
+
+      const expectedValidationErrors = mapAllValidationErrorResponses({
+        counterparties: mockResponse,
+        repaymentProfiles: mockResponse,
+      });
+
+      const expected = {
+        status: HttpStatus.BAD_REQUEST,
+        data: {
+          statusCode: HttpStatus.BAD_REQUEST,
+          message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+          validationErrors: expectedValidationErrors,
+        },
+      };
+
+      expect(response).toEqual(expected);
+    });
+  });
+
+  describe(`when a service returns a first item with a status that is ${HttpStatus.BAD_REQUEST}`, () => {
+    const mockResponse = [
+      {
+        status: HttpStatus.BAD_REQUEST,
+        data: {
+          validationErrors: [{ message: 'mock counterparty validation error' }],
+        },
+      },
+    ] as AxiosResponse[];
+
+    describe('counterpartyService.createMany', () => {
+      beforeEach(() => {
+        createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        counterpartyService.createMany = createCounterpartiesSpy;
+      });
+
+      it('should return an object with mapped counterparty errors', async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.COUNTERPARTY,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.BAD_REQUEST,
+          data: {
+            statusCode: HttpStatus.BAD_REQUEST,
+            message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+
+    describe('repaymentProfiles.createMany', () => {
+      beforeEach(() => {
+        createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        repaymentProfileService.createMany = createRepaymentProfilesSpy;
+      });
+
+      it('should return an object with mapped repayment profile errors', async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.BAD_REQUEST,
+          data: {
+            statusCode: HttpStatus.BAD_REQUEST,
+            message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+  });
+
+  describe(`when a service returns a first item with a status that is NOT ${HttpStatus.BAD_REQUEST}`, () => {
+    const mockMessage = 'The service is unavailable';
+
+    const mockResponse = [
+      {
+        status: HttpStatus.SERVICE_UNAVAILABLE,
+        data: { message: mockMessage },
+      },
+    ] as AxiosResponse[];
+
+    describe('counterpartyService.createMany', () => {
+      beforeEach(() => {
+        createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        counterpartyService.createMany = createCounterpartiesSpy;
+      });
+
+      it("should return an object with the first counterparty's status and message", async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.COUNTERPARTY,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.SERVICE_UNAVAILABLE,
+          data: {
+            statusCode: HttpStatus.SERVICE_UNAVAILABLE,
+            message: mockMessage,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+
+    describe('repaymentProfiles.createMany', () => {
+      beforeEach(() => {
+        createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockResponse);
+
+        repaymentProfileService.createMany = createRepaymentProfilesSpy;
+      });
+
+      it("should return an object with the first repayment profille's status and message", async () => {
+        const response = await service.createFacility(mockPayload);
+
+        const expectedValidationErrors = mapValidationErrorResponses({
+          entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+          responses: mockResponse,
+        });
+
+        const expected = {
+          status: HttpStatus.SERVICE_UNAVAILABLE,
+          data: {
+            statusCode: HttpStatus.SERVICE_UNAVAILABLE,
+            message: mockMessage,
+            validationErrors: expectedValidationErrors,
+          },
+        };
+
+        expect(response).toEqual(expected);
+      });
+    });
+  });
+});
diff --git a/src/modules/gift/gift.service-createFacility-error-handling.test.ts b/src/modules/gift/gift.service-createFacility-error-handling.test.ts
new file mode 100644
index 00000000..13a85a28
--- /dev/null
+++ b/src/modules/gift/gift.service-createFacility-error-handling.test.ts
@@ -0,0 +1,140 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES } from '@ukef/constants';
+import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - error handling', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
+
+    counterpartyService.createMany = createCounterpartiesSpy;
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+    service.createInitialFacility = createInitialFacilitySpy;
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('when giftService.createInitialFacility throws an error', () => {
+    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
+
+    const mockAxiosErrorData = {
+      validationErrors: [{ mock: true }],
+    };
+
+    beforeEach(() => {
+      createInitialFacilitySpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
+
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+      service.createInitialFacility = createInitialFacilitySpy;
+    });
+
+    it('should throw an error', async () => {
+      const response = service.createFacility(mockPayload);
+
+      const expected = 'Error creating GIFT facility';
+
+      await expect(response).rejects.toThrow(expected);
+    });
+  });
+
+  describe('when counterpartyService.createMany throws an error', () => {
+    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
+
+    const mockAxiosErrorData = {
+      validationErrors: [{ mock: true }],
+    };
+
+    beforeEach(() => {
+      createCounterpartiesSpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
+
+      counterpartyService.createMany = createCounterpartiesSpy;
+
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+      service.createInitialFacility = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    });
+
+    it('should throw an error', async () => {
+      const response = service.createFacility(mockPayload);
+
+      const expected = 'Error creating GIFT facility';
+
+      await expect(response).rejects.toThrow(expected);
+    });
+  });
+
+  describe('when repaymentProfileService.createMany throws an error', () => {
+    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
+
+    const mockAxiosErrorData = {
+      validationErrors: [{ mock: true }],
+    };
+
+    beforeEach(() => {
+      createRepaymentProfilesSpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
+
+      repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+      service.createInitialFacility = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    });
+
+    it('should throw an error', async () => {
+      const response = service.createFacility(mockPayload);
+
+      const expected = 'Error creating GIFT facility';
+
+      await expect(response).rejects.toThrow(expected);
+    });
+  });
+});
diff --git a/src/modules/gift/gift.service-createFacility.test.ts b/src/modules/gift/gift.service-createFacility.test.ts
index b461d79a..c0d80ab9 100644
--- a/src/modules/gift/gift.service-createFacility.test.ts
+++ b/src/modules/gift/gift.service-createFacility.test.ts
@@ -1,33 +1,36 @@
 import { HttpService } from '@nestjs/axios';
 import { HttpStatus } from '@nestjs/common';
-import { EXAMPLES, GIFT } from '@ukef/constants';
-import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
-import { AxiosResponse } from 'axios';
+import { EXAMPLES } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
 
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
-import { mapValidationErrorResponses } from './helpers';
 
 const {
-  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload },
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
 } = EXAMPLES;
 
-const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
-
 const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
 
 const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
 
 const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
 
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
 describe('GiftService.createFacility', () => {
   let httpService: HttpService;
   let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
   let service: GiftService;
 
   let giftHttpService;
   let mockHttpServicePost: jest.Mock;
-  let mockCreateCounterparties: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
 
   beforeEach(() => {
     httpService = new HttpService();
@@ -41,34 +44,25 @@ describe('GiftService.createFacility', () => {
     };
 
     counterpartyService = new GiftCounterpartyService(giftHttpService);
-
-    mockCreateCounterparties = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
-
-    counterpartyService.createMany = mockCreateCounterparties;
-
-    service = new GiftService(giftHttpService, counterpartyService);
-  });
-
-  afterAll(() => {
-    jest.resetAllMocks();
-  });
-
-  let createInitialFacilitySpy: jest.Mock;
-  let createCounterpartiesSpy: jest.Mock;
-
-  beforeEach(() => {
-    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponse201());
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
 
     createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
     createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
 
     counterpartyService.createMany = createCounterpartiesSpy;
 
-    service = new GiftService(giftHttpService, counterpartyService);
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
 
     service.createInitialFacility = createInitialFacilitySpy;
   });
 
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
   it('should call service.createInitialFacility', async () => {
     await service.createFacility(mockPayload);
 
@@ -85,6 +79,14 @@ describe('GiftService.createFacility', () => {
     expect(createCounterpartiesSpy).toHaveBeenCalledWith(mockPayload.counterparties, FACILITY_RESPONSE_DATA.workPackageId);
   });
 
+  it('should call giftRepaymentProfileService.createMany', async () => {
+    await service.createFacility(mockPayload);
+
+    expect(createRepaymentProfilesSpy).toHaveBeenCalledTimes(1);
+
+    expect(createRepaymentProfilesSpy).toHaveBeenCalledWith(mockPayload.repaymentProfiles, FACILITY_RESPONSE_DATA.workPackageId);
+  });
+
   describe('when all calls are successful', () => {
     it('should return a response object', async () => {
       const response = await service.createFacility(mockPayload);
@@ -94,175 +96,11 @@ describe('GiftService.createFacility', () => {
         data: {
           ...FACILITY_RESPONSE_DATA,
           counterparties: mockCounterparties,
+          repaymentProfiles: mockRepaymentProfiles,
         },
       };
 
       expect(response).toEqual(expected);
     });
   });
-
-  describe('when counterpartyService.createInitialFacility throws an error', () => {
-    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
-
-    const mockAxiosErrorData = {
-      validationErrors: [{ mock: true }],
-    };
-
-    beforeEach(() => {
-      createInitialFacilitySpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
-
-      service = new GiftService(giftHttpService, counterpartyService);
-
-      service.createInitialFacility = createInitialFacilitySpy;
-    });
-
-    it('should throw an error', async () => {
-      const response = service.createFacility(mockPayload);
-
-      const expected = 'Error creating GIFT facility';
-
-      await expect(response).rejects.toThrow(expected);
-    });
-  });
-
-  describe(`when counterpartyService.createMany returns a first item with a status that is ${HttpStatus.CREATED} and other items have errors`, () => {
-    const mockCounterpartiesResponse = [
-      {
-        status: HttpStatus.CREATED,
-        data: {},
-      },
-      {
-        status: HttpStatus.BAD_REQUEST,
-        data: {
-          validationErrors: [{ message: 'mock validation error' }],
-        },
-      },
-    ] as AxiosResponse[];
-
-    beforeEach(() => {
-      createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCounterpartiesResponse);
-
-      counterpartyService.createMany = createCounterpartiesSpy;
-    });
-
-    it('should return an object with mapped errors', async () => {
-      const response = await service.createFacility(mockPayload);
-
-      const expectedValidationErrors = mapValidationErrorResponses({
-        entityName: ENTITY_NAMES.COUNTERPARTY,
-        responses: mockCounterpartiesResponse,
-      });
-
-      const expected = {
-        status: HttpStatus.BAD_REQUEST,
-        data: {
-          statusCode: HttpStatus.BAD_REQUEST,
-          message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
-          validationErrors: expectedValidationErrors,
-        },
-      };
-
-      expect(response).toEqual(expected);
-    });
-  });
-
-  describe(`when counterpartyService.createMany returns a first item with a status that is ${HttpStatus.BAD_REQUEST}`, () => {
-    const mockCounterpartiesResponse = [
-      {
-        status: HttpStatus.BAD_REQUEST,
-        data: {
-          validationErrors: [{ message: 'mock validation error' }],
-        },
-      },
-    ] as AxiosResponse[];
-
-    beforeEach(() => {
-      createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCounterpartiesResponse);
-
-      counterpartyService.createMany = createCounterpartiesSpy;
-    });
-
-    it('should return an object with mapped errors', async () => {
-      const response = await service.createFacility(mockPayload);
-
-      const expectedValidationErrors = mapValidationErrorResponses({
-        entityName: ENTITY_NAMES.COUNTERPARTY,
-        responses: mockCounterpartiesResponse,
-      });
-
-      const expected = {
-        status: HttpStatus.BAD_REQUEST,
-        data: {
-          statusCode: HttpStatus.BAD_REQUEST,
-          message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
-          validationErrors: expectedValidationErrors,
-        },
-      };
-
-      expect(response).toEqual(expected);
-    });
-  });
-
-  describe(`when counterpartyService.createMany returns a first item with a status that is NOT ${HttpStatus.BAD_REQUEST}`, () => {
-    const mockMessage = 'The service is unavailable';
-
-    const mockCounterpartiesResponse = [
-      {
-        status: HttpStatus.SERVICE_UNAVAILABLE,
-        data: { message: mockMessage },
-      },
-    ] as AxiosResponse[];
-
-    beforeEach(() => {
-      createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCounterpartiesResponse);
-
-      counterpartyService.createMany = createCounterpartiesSpy;
-    });
-
-    it("should return an object with the first counterparty's status and message", async () => {
-      const response = await service.createFacility(mockPayload);
-
-      const expectedValidationErrors = mapValidationErrorResponses({
-        entityName: ENTITY_NAMES.COUNTERPARTY,
-        responses: mockCounterpartiesResponse,
-      });
-
-      const expected = {
-        status: HttpStatus.SERVICE_UNAVAILABLE,
-        data: {
-          statusCode: HttpStatus.SERVICE_UNAVAILABLE,
-          message: mockMessage,
-          validationErrors: expectedValidationErrors,
-        },
-      };
-
-      expect(response).toEqual(expected);
-    });
-  });
-
-  describe('when counterpartyService.createMany throws an error', () => {
-    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
-
-    const mockAxiosErrorData = {
-      validationErrors: [{ mock: true }],
-    };
-
-    beforeEach(() => {
-      createCounterpartiesSpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
-
-      counterpartyService.createMany = createCounterpartiesSpy;
-
-      service = new GiftService(giftHttpService, counterpartyService);
-
-      service.createInitialFacility = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
-    });
-
-    it('should throw an error', async () => {
-      const response = service.createFacility(mockPayload);
-
-      const expected = 'Error creating GIFT facility';
-
-      await expect(response).rejects.toThrow(expected);
-    });
-  });
 });
diff --git a/src/modules/gift/gift.service-createInitialFacility.test.ts b/src/modules/gift/gift.service-createInitialFacility.test.ts
index f9a252b9..7b4af583 100644
--- a/src/modules/gift/gift.service-createInitialFacility.test.ts
+++ b/src/modules/gift/gift.service-createInitialFacility.test.ts
@@ -3,6 +3,7 @@ import { EXAMPLES, GIFT } from '@ukef/constants';
 import { mockResponse201, mockResponse500 } from '@ukef-test/http-response';
 
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
 
 const {
@@ -16,6 +17,7 @@ const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
 describe('GiftService.createInitialFacility', () => {
   let httpService: HttpService;
   let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
   let service: GiftService;
 
   let giftHttpService;
@@ -33,8 +35,9 @@ describe('GiftService.createInitialFacility', () => {
     };
 
     counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
 
-    service = new GiftService(giftHttpService, counterpartyService);
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
   });
 
   afterAll(() => {
@@ -68,7 +71,7 @@ describe('GiftService.createInitialFacility', () => {
 
       giftHttpService.post = mockHttpServicePost;
 
-      service = new GiftService(giftHttpService, counterpartyService);
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
     });
 
     it('should thrown an error', async () => {
diff --git a/src/modules/gift/gift.service-getFacility.test.ts b/src/modules/gift/gift.service-getFacility.test.ts
index 6b3182f9..7bec3012 100644
--- a/src/modules/gift/gift.service-getFacility.test.ts
+++ b/src/modules/gift/gift.service-getFacility.test.ts
@@ -3,6 +3,7 @@ import { EXAMPLES, GIFT } from '@ukef/constants';
 import { mockResponse200, mockResponse500 } from '@ukef-test/http-response';
 
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
 
 const {
@@ -16,6 +17,7 @@ const mockResponseGet = mockResponse200(FACILITY_RESPONSE_DATA);
 describe('GiftService.getFacility', () => {
   let httpService: HttpService;
   let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
   let service: GiftService;
 
   let giftHttpService;
@@ -33,8 +35,9 @@ describe('GiftService.getFacility', () => {
     };
 
     counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
 
-    service = new GiftService(giftHttpService, counterpartyService);
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
   });
 
   afterAll(() => {
@@ -67,7 +70,7 @@ describe('GiftService.getFacility', () => {
 
       giftHttpService.get = mockHttpServiceGet;
 
-      service = new GiftService(giftHttpService, counterpartyService);
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
     });
 
     it('should thrown an error', async () => {
diff --git a/src/modules/gift/gift.service.ts b/src/modules/gift/gift.service.ts
index 2efc3e67..4864c16d 100644
--- a/src/modules/gift/gift.service.ts
+++ b/src/modules/gift/gift.service.ts
@@ -5,10 +5,11 @@ import { AxiosResponse } from 'axios';
 
 import { GiftFacilityCreationDto, GiftFacilityDto } from './dto';
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftHttpService } from './gift-http.service';
-import { mapResponsesData, mapValidationErrorResponses } from './helpers';
+import { mapAllValidationErrorResponses, mapResponsesData } from './helpers';
 
-const { API_RESPONSE_MESSAGES, ENTITY_NAMES, PATH } = GIFT;
+const { API_RESPONSE_MESSAGES, PATH } = GIFT;
 
 interface CreateFacilityResponse {
   status: AxiosResponse['status'];
@@ -24,9 +25,11 @@ export class GiftService {
   constructor(
     private readonly giftHttpService: GiftHttpService,
     private readonly giftCounterpartyService: GiftCounterpartyService,
+    private readonly giftRepaymentProfileService: GiftRepaymentProfileService,
   ) {
     this.giftHttpService = giftHttpService;
     this.giftCounterpartyService = giftCounterpartyService;
+    this.giftRepaymentProfileService = giftRepaymentProfileService;
   }
 
   /**
@@ -76,7 +79,7 @@ export class GiftService {
    */
   async createFacility(data: GiftFacilityCreationDto): Promise<CreateFacilityResponse> {
     try {
-      const { overview, counterparties: counterpartiesPayload } = data;
+      const { overview, counterparties: counterpartiesPayload, repaymentProfiles: repaymentProfilesPayload } = data;
 
       const { data: facility, status } = await this.createInitialFacility(overview);
 
@@ -93,11 +96,15 @@ export class GiftService {
         };
       }
 
-      const counterparties = await this.giftCounterpartyService.createMany(counterpartiesPayload, facility.workPackageId);
+      const { workPackageId } = facility;
 
-      const validationErrors = mapValidationErrorResponses({
-        entityName: ENTITY_NAMES.COUNTERPARTY,
-        responses: counterparties,
+      const counterparties = await this.giftCounterpartyService.createMany(counterpartiesPayload, workPackageId);
+
+      const repaymentProfiles = await this.giftRepaymentProfileService.createMany(repaymentProfilesPayload, workPackageId);
+
+      const validationErrors = mapAllValidationErrorResponses({
+        counterparties,
+        repaymentProfiles,
       });
 
       if (validationErrors.length) {
@@ -126,13 +133,12 @@ export class GiftService {
         };
       }
 
-      const mappedCounterparties = mapResponsesData(counterparties);
-
       return {
         status: HttpStatus.CREATED,
         data: {
           ...facility,
-          counterparties: mappedCounterparties,
+          counterparties: mapResponsesData(counterparties),
+          repaymentProfiles: mapResponsesData(repaymentProfiles),
         },
       };
     } catch (error) {
diff --git a/src/modules/gift/helpers/index.ts b/src/modules/gift/helpers/index.ts
index d90f2047..3b02f7d6 100644
--- a/src/modules/gift/helpers/index.ts
+++ b/src/modules/gift/helpers/index.ts
@@ -1,2 +1,3 @@
+export * from './map-all-validation-error-responses';
 export * from './map-responses-data';
 export * from './map-validation-error-responses';
diff --git a/src/modules/gift/helpers/map-all-validation-error-responses/index.test.ts b/src/modules/gift/helpers/map-all-validation-error-responses/index.test.ts
new file mode 100644
index 00000000..1e11ba7a
--- /dev/null
+++ b/src/modules/gift/helpers/map-all-validation-error-responses/index.test.ts
@@ -0,0 +1,39 @@
+import { HttpStatus } from '@nestjs/common';
+import { GIFT } from '@ukef/constants';
+import { AxiosResponse } from 'axios';
+
+import { mapValidationErrorResponses } from '../map-validation-error-responses';
+import { mapAllValidationErrorResponses } from '.';
+
+const { ENTITY_NAMES } = GIFT;
+
+const mockValidationErrors = [
+  {
+    path: ['exitDate'],
+    message: 'Expected string, received number',
+  },
+];
+
+describe('modules/gift/helpers/map-all-validation-error-responses', () => {
+  it('should return multiple mapValidationErrorResponses results', () => {
+    const mockResponses = [
+      { status: HttpStatus.CREATED },
+      { status: HttpStatus.BAD_REQUEST, data: { validationErrors: mockValidationErrors } },
+    ] as AxiosResponse[];
+
+    const result = mapAllValidationErrorResponses({ counterparties: mockResponses, repaymentProfiles: mockResponses });
+
+    const expected = [
+      ...mapValidationErrorResponses({
+        entityName: ENTITY_NAMES.COUNTERPARTY,
+        responses: mockResponses,
+      }),
+      ...mapValidationErrorResponses({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        responses: mockResponses,
+      }),
+    ];
+
+    expect(result).toEqual(expected);
+  });
+});
diff --git a/src/modules/gift/helpers/map-all-validation-error-responses/index.ts b/src/modules/gift/helpers/map-all-validation-error-responses/index.ts
new file mode 100644
index 00000000..fff51e41
--- /dev/null
+++ b/src/modules/gift/helpers/map-all-validation-error-responses/index.ts
@@ -0,0 +1,28 @@
+import { GIFT } from '@ukef/constants';
+import { ValidationErrorResponse } from '@ukef/types';
+import { AxiosResponse } from 'axios';
+
+import { mapValidationErrorResponses } from '../map-validation-error-responses';
+
+const { ENTITY_NAMES } = GIFT;
+
+interface MapAllValidationErrorResponsesParams {
+  counterparties: AxiosResponse[];
+  repaymentProfiles: AxiosResponse[];
+}
+
+/**
+ * Map multiple responses with an invalid status.
+ * @param {MapAllValidationErrorResponsesParams}
+ * @returns {ValidationErrorResponse[]} Mapped validation error responses.
+ */
+export const mapAllValidationErrorResponses = ({ counterparties, repaymentProfiles }: MapAllValidationErrorResponsesParams): ValidationErrorResponse[] => [
+  ...mapValidationErrorResponses({
+    entityName: ENTITY_NAMES.COUNTERPARTY,
+    responses: counterparties,
+  }),
+  ...mapValidationErrorResponses({
+    entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+    responses: repaymentProfiles,
+  }),
+];
diff --git a/src/modules/gift/helpers/map-validation-error-responses/index.ts b/src/modules/gift/helpers/map-validation-error-responses/index.ts
index 765e9253..d5b57403 100644
--- a/src/modules/gift/helpers/map-validation-error-responses/index.ts
+++ b/src/modules/gift/helpers/map-validation-error-responses/index.ts
@@ -1,5 +1,6 @@
 import { HttpStatus } from '@nestjs/common';
 import { GIFT } from '@ukef/constants';
+import { ValidationErrorResponse } from '@ukef/types';
 import { AxiosResponse } from 'axios';
 
 const { API_RESPONSE_TYPES } = GIFT;
@@ -9,25 +10,11 @@ interface MapValidationErrorResponsesParams {
   responses: AxiosResponse[];
 }
 
-interface GiftValidationError {
-  path: string[];
-  message: string;
-}
-
-interface ValidationErrorResponse {
-  entityName: string;
-  index: number;
-  message: string;
-  status: number;
-  type: string;
-  validationErrors: GiftValidationError[];
-}
-
 /**
  * Map responses with an invalid status.
  * An index is also returned to help consumers debug which item in an array has errors.
  * @param {MapValidationErrorResponsesParams}
- * @returns {ExtendedAxiosResponse[]} Mapped responses that do not have a CREATED status.
+ * @returns {ValidationErrorResponse[]} Mapped responses that do not have a CREATED status.
  */
 export const mapValidationErrorResponses = ({ responses, entityName }: MapValidationErrorResponsesParams): ValidationErrorResponse[] => {
   const mappedAndFiltered: ValidationErrorResponse[] = [];
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index b2e25d71..9b5812d2 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -3,3 +3,17 @@ export interface ExternalServiceConfig {
   maxRedirects: number;
   timeout: number;
 }
+
+interface GiftValidationError {
+  path: string[];
+  message: string;
+}
+
+interface ValidationErrorResponse {
+  entityName: string;
+  index: number;
+  message: string;
+  status: number;
+  type: string;
+  validationErrors: GiftValidationError[];
+}
diff --git a/test/gift/create-facility-validation-counterparties.api-test.ts b/test/gift/create-facility-validation-counterparties.api-test.ts
index 672f3cfe..73ac6c8f 100644
--- a/test/gift/create-facility-validation-counterparties.api-test.ts
+++ b/test/gift/create-facility-validation-counterparties.api-test.ts
@@ -1,3 +1,4 @@
+import { HttpStatus } from '@nestjs/common';
 import AppConfig from '@ukef/config/app.config';
 import { EXAMPLES, GIFT } from '@ukef/constants';
 import { Api } from '@ukef-test/support/api';
@@ -41,7 +42,7 @@ describe('POST /gift/facility - validation - counterparties', () => {
   };
 
   describe('when an empty counterparty object is provided', () => {
-    it('should return a 400 response with validation errors for all required fields', async () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with validation errors for all required fields`, async () => {
       const mockPayload = {
         ...EXAMPLES.GIFT.FACILITY_CREATION_PAYLOAD,
         counterparties: [{}],
@@ -49,7 +50,7 @@ describe('POST /gift/facility - validation - counterparties', () => {
 
       const { status, body } = await api.post(url, mockPayload);
 
-      expect(status).toBe(400);
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
 
       const expected = {
         error: 'Bad Request',
@@ -71,7 +72,7 @@ describe('POST /gift/facility - validation - counterparties', () => {
           `counterparties.0.startDate must be longer than or equal to ${COUNTERPARTY_VALIDATION.START_DATE.MIN_LENGTH} characters`,
           `counterparties.0.startDate must be a string`,
         ],
-        statusCode: 400,
+        statusCode: HttpStatus.BAD_REQUEST,
       };
 
       expect(body).toStrictEqual(expected);
diff --git a/test/gift/create-facility-validation.api-test.ts b/test/gift/create-facility-validation.api-test.ts
index ba6f0a66..967c0cd2 100644
--- a/test/gift/create-facility-validation.api-test.ts
+++ b/test/gift/create-facility-validation.api-test.ts
@@ -1,3 +1,4 @@
+import { HttpStatus } from '@nestjs/common';
 import AppConfig from '@ukef/config/app.config';
 import { EXAMPLES, GIFT } from '@ukef/constants';
 import { Api } from '@ukef-test/support/api';
@@ -35,12 +36,12 @@ describe('POST /gift/facility - validation', () => {
   });
 
   describe('when an empty object provided', () => {
-    it('should return a 400 response with a validation error', async () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with a validation error`, async () => {
       const mockPayload = {};
 
       const { status, body } = await api.post(url, mockPayload);
 
-      expect(status).toBe(400);
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
 
       const expected = {
         error: 'Bad Request',
@@ -50,8 +51,11 @@ describe('POST /gift/facility - validation', () => {
           'counterparties should not be null or undefined',
           'counterparties should not be empty',
           'counterparties must be an array',
+          'repaymentProfiles should not be null or undefined',
+          'repaymentProfiles should not be empty',
+          'repaymentProfiles must be an array',
         ],
-        statusCode: 400,
+        statusCode: HttpStatus.BAD_REQUEST,
       };
 
       expect(body).toStrictEqual(expected);
@@ -59,12 +63,12 @@ describe('POST /gift/facility - validation', () => {
   });
 
   describe('when an unpopulated object is provided', () => {
-    it('should return a 400 response with validation errors for all required fields', async () => {
-      const mockPayload = { overview: {}, counterparties: [] };
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with validation errors for all required fields`, async () => {
+      const mockPayload = { overview: {}, counterparties: [], repaymentProfiles: [] };
 
       const { status, body } = await api.post(url, mockPayload);
 
-      expect(status).toBe(400);
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
 
       const expected = {
         error: 'Bad Request',
@@ -102,8 +106,9 @@ describe('POST /gift/facility - validation', () => {
           `overview.productType must be longer than or equal to ${OVERVIEW_VALIDATION.PRODUCT_TYPE.MIN_LENGTH} characters`,
           'overview.productType must be a string',
           'counterparties should not be empty',
+          'repaymentProfiles should not be empty',
         ],
-        statusCode: 400,
+        statusCode: HttpStatus.BAD_REQUEST,
       };
 
       expect(body).toStrictEqual(expected);
diff --git a/test/gift/create-facility.api-test.ts b/test/gift/create-facility.api-test.ts
index 683fec38..0256b8e1 100644
--- a/test/gift/create-facility.api-test.ts
+++ b/test/gift/create-facility.api-test.ts
@@ -34,6 +34,7 @@ describe('POST /gift/facility', () => {
       ],
     },
     counterparty: { aCounterparty: true },
+    repaymentProfile: { aRepaymentProfile: true },
     facility: {
       facilityId: mockFacilityId,
       workPackageId: mockWorkPackageId,
@@ -50,9 +51,11 @@ describe('POST /gift/facility', () => {
   };
 
   const payloadCounterparties = Object.keys(GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD.counterparties);
+  const payloadRepaymentProfiles = Object.keys(GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD.repaymentProfiles);
 
   const facilityUrl = `/api/${prefixAndVersion}/gift${PATH.FACILITY}`;
   const counterPartyUrl = `${PATH.WORK_PACKAGE}/${mockWorkPackageId}${PATH.COUNTERPARTY}${PATH.CREATION_EVENT}`;
+  const repaymentProfileUrl = `${PATH.WORK_PACKAGE}/${mockWorkPackageId}${PATH.REPAYMENT_PROFILE}${PATH.MANUAL}${PATH.CREATION_EVENT}`;
 
   let api: Api;
 
@@ -75,12 +78,14 @@ describe('POST /gift/facility', () => {
       api.postWithoutAuth(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD, incorrectAuth?.headerName, incorrectAuth?.headerValue),
   });
 
-  describe('when the payload is valid and a 201 response is returned by GIFT', () => {
-    it('should return a 201 response with a facility and counterparties', async () => {
+  describe(`when the payload is valid and a ${HttpStatus.CREATED} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.CREATED}} response with a facility, counterparties and repayment profiles`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
 
       nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
 
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.CREATED, mockResponses.repaymentProfile);
+
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
 
       expect(status).toBe(HttpStatus.CREATED);
@@ -88,14 +93,15 @@ describe('POST /gift/facility', () => {
       const expected = {
         ...mockResponses.facility,
         counterparties: Array(payloadCounterparties.length).fill(mockResponses.counterparty),
+        repaymentProfiles: Array(payloadRepaymentProfiles.length).fill(mockResponses.repaymentProfile),
       };
 
       expect(body).toStrictEqual(expected);
     });
   });
 
-  describe('when a 400 response is returned by the GIFT facility endpoint', () => {
-    it('should return a 400 response', async () => {
+  describe(`when a ${HttpStatus.BAD_REQUEST} response is returned by the GIFT facility endpoint`, () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
 
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
@@ -108,8 +114,8 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when a 401 response is returned by the GIFT facility endpoint', () => {
-    it('should return a 401 response', async () => {
+  describe(`when a ${HttpStatus.UNAUTHORIZED} response is returned by the GIFT facility endpoint`, () => {
+    it(`should return a ${HttpStatus.UNAUTHORIZED} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
 
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
@@ -121,7 +127,7 @@ describe('POST /gift/facility', () => {
   });
 
   describe('when an unacceptable status is returned by the GIFT facility endpoint', () => {
-    it('should return a 500 response', async () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.I_AM_A_TEAPOT);
 
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
@@ -132,8 +138,8 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when a 500 status is returned by the GIFT facility endpoint', () => {
-    it('should return a 500 response', async () => {
+  describe(`when a ${HttpStatus.INTERNAL_SERVER_ERROR} status is returned by the GIFT facility endpoint`, () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.INTERNAL_SERVER_ERROR);
 
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
@@ -144,12 +150,14 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when a 400 response is returned by the GIFT counterparty endpoint', () => {
-    it('should return a 400 response with a mapped body/validation errors', async () => {
+  describe(`when a ${HttpStatus.BAD_REQUEST} response is returned by the GIFT counterparty endpoint`, () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with a mapped body/validation errors`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
 
       nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
 
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.CREATED, mockResponses.repaymentProfile);
+
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
 
       expect(status).toBe(HttpStatus.BAD_REQUEST);
@@ -173,12 +181,14 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when a 401 response is returned by the GIFT counterparty endpoint', () => {
-    it('should return a 401 response  with a mapped body/validation errors', async () => {
+  describe(`when a ${HttpStatus.UNAUTHORIZED} response is returned by the GIFT counterparty endpoint`, () => {
+    it(`should return a ${HttpStatus.UNAUTHORIZED} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
 
       nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
 
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.CREATED, mockResponses.repaymentProfile);
+
       const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
 
       expect(status).toBe(HttpStatus.UNAUTHORIZED);
@@ -200,8 +210,8 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when an unacceptable status is returned by the GIFT counterparty endpoint', () => {
-    it('should return a 500 response', async () => {
+  describe(`when a ${HttpStatus.INTERNAL_SERVER_ERROR} status is returned by the GIFT counterparty endpoint`, () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
 
       nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.INTERNAL_SERVER_ERROR, mockResponses.badRequest);
@@ -214,8 +224,8 @@ describe('POST /gift/facility', () => {
     });
   });
 
-  describe('when a 500 response is returned by the GIFT counterparty endpoint', () => {
-    it('should return a 500 response', async () => {
+  describe('when an unacceptable response is returned by the GIFT counterparty endpoint', () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
       nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
 
       nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.I_AM_A_TEAPOT);
@@ -227,4 +237,96 @@ describe('POST /gift/facility', () => {
       expect(body).toStrictEqual(mockResponses.internalServerError);
     });
   });
+
+  describe(`when a ${HttpStatus.BAD_REQUEST} response is returned by the GIFT repayment profile endpoint`, () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with a mapped body/validation errors`, async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.badRequest.message,
+        validationErrors: mockResponses.badRequest.validationErrors,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.badRequest.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.badRequest,
+        message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe(`when a ${HttpStatus.UNAUTHORIZED} response is returned by the GIFT repayment profile endpoint`, () => {
+    it(`should return a ${HttpStatus.UNAUTHORIZED} response`, async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.UNAUTHORIZED);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.unauthorized.message,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.unauthorized.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.unauthorized,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe(`when a ${HttpStatus.INTERNAL_SERVER_ERROR} status is returned by the GIFT repayment profile endpoint`, () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.badRequest);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.INTERNAL_SERVER_ERROR, mockResponses.badRequest);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
+
+      expect(body).toStrictEqual(mockResponses.internalServerError);
+    });
+  });
+
+  describe(`when an unacceptable response is returned by the GIFT repayment profile endpoint`, () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.I_AM_A_TEAPOT);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
+
+      expect(body).toStrictEqual(mockResponses.internalServerError);
+    });
+  });
 });
diff --git a/test/gift/get-facility-by-id.api-test.ts b/test/gift/get-facility-by-id.api-test.ts
index 4f68d2d9..a053c493 100644
--- a/test/gift/get-facility-by-id.api-test.ts
+++ b/test/gift/get-facility-by-id.api-test.ts
@@ -1,3 +1,4 @@
+import { HttpStatus } from '@nestjs/common';
 import AppConfig from '@ukef/config/app.config';
 import { GIFT } from '@ukef/constants';
 import { IncorrectAuthArg, withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests';
@@ -54,8 +55,8 @@ describe('GET /gift/facility/{facilityId}', () => {
     });
   });
 
-  describe('when a 200 response is returned by GIFT', () => {
-    it('should return a 200 response with the received data', async () => {
+  describe(`when a ${HttpStatus.OK} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.OK} response with the received data`, async () => {
       const mockResponse = {
         facilityId: mockFacilityId,
         aMockFacility: true,
@@ -65,16 +66,16 @@ describe('GET /gift/facility/{facilityId}', () => {
 
       const { status, body } = await api.get(url);
 
-      expect(status).toBe(200);
+      expect(status).toBe(HttpStatus.OK);
 
       expect(body).toStrictEqual(mockResponse);
     });
   });
 
-  describe('when a 400 response is returned by GIFT', () => {
-    it('should return a 400 response with the facility', async () => {
+  describe(`when a ${HttpStatus.BAD_REQUEST} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.BAD_REQUEST} response with the facility`, async () => {
       const mockResponse = {
-        statusCode: 400,
+        statusCode: HttpStatus.BAD_REQUEST,
         message: 'Validation error',
         validationErrors: [
           {
@@ -88,16 +89,16 @@ describe('GET /gift/facility/{facilityId}', () => {
 
       const { status, body } = await api.get(url);
 
-      expect(status).toBe(400);
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
 
       expect(body).toStrictEqual(mockResponse);
     });
   });
 
-  describe('when a 404 response is returned by GIFT', () => {
-    it('should return a 404 response with the facility', async () => {
+  describe(`when a ${HttpStatus.NOT_FOUND} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.NOT_FOUND} response with the facility`, async () => {
       const mockResponse = {
-        statusCode: 404,
+        statusCode: HttpStatus.NOT_FOUND,
         message: 'No Facility was found',
       };
 
@@ -105,29 +106,29 @@ describe('GET /gift/facility/{facilityId}', () => {
 
       const { status, body } = await api.get(url);
 
-      expect(status).toBe(404);
+      expect(status).toBe(HttpStatus.NOT_FOUND);
 
       expect(body).toStrictEqual(mockResponse);
     });
   });
 
-  describe('when a 401 response is returned by GIFT', () => {
-    it('should return a 401 response', async () => {
-      nock(GIFT_API_URL).get(`${FACILITY}/${mockFacilityId}`).reply(401);
+  describe(`when a ${HttpStatus.UNAUTHORIZED} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.UNAUTHORIZED} response`, async () => {
+      nock(GIFT_API_URL).get(`${FACILITY}/${mockFacilityId}`).reply(HttpStatus.UNAUTHORIZED);
 
       const { status } = await api.get(url);
 
-      expect(status).toBe(401);
+      expect(status).toBe(HttpStatus.UNAUTHORIZED);
     });
   });
 
-  describe('when a 500 response is returned by GIFT', () => {
-    it('should return a 500 response', async () => {
-      nock(GIFT_API_URL).get(`${FACILITY}/${mockFacilityId}`).reply(500);
+  describe(`when a ${HttpStatus.INTERNAL_SERVER_ERROR} response is returned by GIFT`, () => {
+    it(`should return a ${HttpStatus.INTERNAL_SERVER_ERROR} response`, async () => {
+      nock(GIFT_API_URL).get(`${FACILITY}/${mockFacilityId}`).reply(HttpStatus.INTERNAL_SERVER_ERROR);
 
       const { status } = await api.get(url);
 
-      expect(status).toBe(500);
+      expect(status).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
     });
   });
 });


Reviews:
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T15:25:54Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T15:26:30Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T15:26:48Z
user: login: abhi-markan
body: Nice work, one minor comment to address please.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T16:46:31Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:25:42Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:57:26Z
user: login: christianmcc-ukef
body: Nice, minor comments and one stream of consciousness that doesn't require action
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T10:47:43Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:58:17Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:58:34Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T13:00:34Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T13:02:30Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T13:05:05Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T13:09:43Z
user: login: abhi-markan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:07:46Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T14:11:17Z
user: login: christianmcc-ukef
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T14:51:44Z
user: login: christianmcc-ukef
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T14:57:28Z
user: login: christianmcc-ukef
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T15:03:10Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T15:28:44Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T15:31:19Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T15:40:32Z
user: login: christianmcc-ukef
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T16:01:39Z
user: login: christianmcc-ukef
body: Nice, LGTM :)
state: APPROVED
author_association: NONE
submitted_at: 2025-04-14T16:02:33Z
user: login: abhi-markan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:16:14Z
user: login: tony-barnes-ukef
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T08:16:36Z
user: login: abhi-markan
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:17:35Z

Review comments:
diff_hunk: @@ -47,5 +50,12 @@ export const GIFT = {
       SHARE_PERCENTAGE: { MIN: 0.1, MAX: 100 },
       START_DATE: DATE_STRING_VALIDATION,
     },
+    REPAYMENT_PROFILE: {
+      NAME: { MIN_LENGTH: 0, MAX_LENGTH: 120 },
+      ALLOCATION: {
+        DUE_DATE: DATE_STRING_VALIDATION,
+        AMOUNT: { MIN: 0 },
path: src/constants/gift.constant.ts
user: login: tony-barnes-ukef
body: This is the same story as the facility amount maximum - needs a decision from BA.

I have added this to the existing facility amount tickets.
created_at: 2025-04-11T15:25:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { IsDefined, IsNumber, IsString, Length, Min } from 'class-validator';
+
+const {
+  GIFT: { REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: {
+    REPAYMENT_PROFILE: { ALLOCATION: VALIDATION },
+  },
+} = GIFT;
+
+/**
+ * GIFT repayment profile allocation DTO.
+ * These fields are required for APIM to create a repayment profile allocation in GIFT.
+ */
+export class GiftRepaymentProfileAllocationDto {
+  @IsDefined()
+  @IsNumber()
+  @Min(VALIDATION.AMOUNT.MIN)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().amount,
+  })
+  amount: number;
+
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.DUE_DATE.MIN_LENGTH, VALIDATION.DUE_DATE.MAX_LENGTH)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().dueDate,
+  })
+  dueDate: string;
path: src/modules/gift/dto/repayment-profile-allocation.ts
user: login: tony-barnes-ukef
body: Next PR will be adding a new decorator to ensure that each `dueDate` provided is unique, as per GIFT validation rules.
created_at: 2025-04-11T15:26:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Next PR will be adding a new decorator to ensure that each provided `name` is unique, as per GIFT validation rules.
created_at: 2025-04-11T15:26:48Z
author_association: COLLABORATOR
diff_hunk: @@ -227,4 +237,96 @@ describe('POST /gift/facility', () => {
       expect(body).toStrictEqual(mockResponses.internalServerError);
     });
   });
+
+  describe('when a 400 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 400 response with a mapped body/validation errors', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.badRequest.message,
+        validationErrors: mockResponses.badRequest.validationErrors,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.badRequest.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.badRequest,
+        message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 401 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 401 response', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.UNAUTHORIZED);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.unauthorized.message,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.unauthorized.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.unauthorized,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 500 status is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 500 response', async () => {
path: test/gift/create-facility.api-test.ts
user: login: abhi-markan
body: ```suggestion
    it(`should return a${HttoStatus.INTERNAL_SERVER_ERROR} response`, async () => {
```

Can this be applied across the PR please.
created_at: 2025-04-11T16:46:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -227,4 +237,96 @@ describe('POST /gift/facility', () => {
       expect(body).toStrictEqual(mockResponses.internalServerError);
     });
   });
+
+  describe('when a 400 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 400 response with a mapped body/validation errors', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.badRequest.message,
+        validationErrors: mockResponses.badRequest.validationErrors,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.badRequest.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.badRequest,
+        message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 401 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 401 response', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.UNAUTHORIZED);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.unauthorized.message,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.unauthorized.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.unauthorized,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 500 status is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 500 response', async () => {
path: test/gift/create-facility.api-test.ts
user: login: tony-barnes-ukef
body: I did think about this - I think it makes digestion harder, but happy to change as both approaches are good IMO.
created_at: 2025-04-14T08:25:42Z
author_association: COLLABORATOR
diff_hunk: @@ -227,4 +237,96 @@ describe('POST /gift/facility', () => {
       expect(body).toStrictEqual(mockResponses.internalServerError);
     });
   });
+
+  describe('when a 400 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 400 response with a mapped body/validation errors', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.BAD_REQUEST, mockResponses.badRequest);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.BAD_REQUEST);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.badRequest.message,
+        validationErrors: mockResponses.badRequest.validationErrors,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.badRequest.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.badRequest,
+        message: API_RESPONSE_MESSAGES.FACILITY_VALIDATION_ERRORS,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 401 response is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 401 response', async () => {
+      nock(GIFT_API_URL).post(PATH.FACILITY).reply(HttpStatus.CREATED, mockResponses.facility);
+
+      nock(GIFT_API_URL).persist().post(counterPartyUrl).reply(HttpStatus.CREATED, mockResponses.counterparty);
+
+      nock(GIFT_API_URL).persist().post(repaymentProfileUrl).reply(HttpStatus.UNAUTHORIZED, mockResponses.unauthorized);
+
+      const { status, body } = await api.post(facilityUrl, GIFT_EXAMPLES.FACILITY_CREATION_PAYLOAD);
+
+      expect(status).toBe(HttpStatus.UNAUTHORIZED);
+
+      const expectedValidationErrors = payloadCounterparties.map((counterparty, index) => ({
+        entityName: ENTITY_NAMES.REPAYMENT_PROFILE,
+        index,
+        message: mockResponses.unauthorized.message,
+        type: API_RESPONSE_TYPES.ERROR,
+        status: mockResponses.unauthorized.statusCode,
+      }));
+
+      const expected = {
+        ...mockResponses.unauthorized,
+        validationErrors: expectedValidationErrors,
+      };
+
+      expect(body).toStrictEqual(expected);
+    });
+  });
+
+  describe('when a 500 status is returned by the GIFT repayment profile endpoint', () => {
+    it('should return a 500 response', async () => {
path: test/gift/create-facility.api-test.ts
user: login: tony-barnes-ukef
body: Applied this to all GIFT API tests.
created_at: 2025-04-14T08:57:26Z
author_association: COLLABORATOR
diff_hunk: @@ -25,14 +31,39 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
path: src/constants/examples/gift.examples.constant.ts
user: login: christianmcc-ukef
body: There's a lot of coupling to other bits of information and constants in this function. 
E.g. the /4 on facility amount which is separate, and then lower down having 2 allocations and 2 profiles which together give 4 amounts that add up to the maximum.
Wondering if instead of trying to de-couple and have an input of facility_amount into this function and number of repayment profiles etc. we just hard-code this a bit more to de-couple but not add a bunch of work?
created_at: 2025-04-14T10:02:46Z
author_association: NONE
diff_hunk: @@ -25,14 +31,39 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
path: src/constants/examples/gift.examples.constant.ts
user: login: christianmcc-ukef
body: Do we just use this for API docs and then a test? Are they used anywhere else? I'm not a fan of this huge examples.constant file growing but given all the test examples in APIM are going to be simple maybe it's not worth a refactor
created_at: 2025-04-14T10:03:58Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { IsDefined, IsNumber, IsString, Length, Min } from 'class-validator';
+
+const {
+  GIFT: { REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: {
+    REPAYMENT_PROFILE: { ALLOCATION: VALIDATION },
+  },
+} = GIFT;
+
+/**
+ * GIFT repayment profile allocation DTO.
+ * These fields are required for APIM to create a repayment profile allocation in GIFT.
+ */
+export class GiftRepaymentProfileAllocationDto {
+  @IsDefined()
+  @IsNumber()
+  @Min(VALIDATION.AMOUNT.MIN)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().amount,
+  })
+  amount: number;
+
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.DUE_DATE.MIN_LENGTH, VALIDATION.DUE_DATE.MAX_LENGTH)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().dueDate,
+  })
+  dueDate: string;
path: src/modules/gift/dto/repayment-profile-allocation.ts
user: login: christianmcc-ukef
body: Do we need that? That feels like business logic APIM can be a bit blind to and GIFT can just handle for us
created_at: 2025-04-14T10:06:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: christianmcc-ukef
body: Likewise do we need it if GIFT is doing this for us and it's GIFT validation
created_at: 2025-04-14T10:07:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
path: src/modules/gift/dto/repayment-profile.ts
user: login: christianmcc-ukef
body: is REPAYMENT_PROFILE not a function here?
created_at: 2025-04-14T10:19:11Z
author_association: NONE
diff_hunk: @@ -1,33 +1,36 @@
 import { HttpService } from '@nestjs/axios';
 import { HttpStatus } from '@nestjs/common';
-import { EXAMPLES, GIFT } from '@ukef/constants';
-import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
-import { AxiosResponse } from 'axios';
+import { EXAMPLES } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
 
 import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
 import { GiftService } from './gift.service';
-import { mapValidationErrorResponses } from './helpers';
 
 const {
-  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload },
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
 } = EXAMPLES;
 
-const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
-
 const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
 
 const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE, REPAYMENT_PROFILE, REPAYMENT_PROFILE];
path: src/modules/gift/gift.service-createFacility.test.ts
user: login: christianmcc-ukef
body: is it `REPAYMENT_PROFILE()`?
created_at: 2025-04-14T10:22:48Z
author_association: NONE
diff_hunk: @@ -25,14 +31,39 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
path: src/constants/examples/gift.examples.constant.ts
user: login: christianmcc-ukef
body: The more I think about it the more I'm going to leave my train of thought for you to see here, but I don't think it's worth moving to something more sophisticated or at least not right now, will resolve this
created_at: 2025-04-14T10:38:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: christianmcc-ukef
body: ```suggestion
const mockInitialCreateResponse = mockResponse201(FACILITY_RESPONSE_DATA);
```
something like that to fit rest of your naming pattern in file
created_at: 2025-04-14T10:40:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - bad requests', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: christianmcc-ukef
body: mockResponse201(FACILITY_RESPONSE_DATA) is defined as a const higher up, can use that const here
created_at: 2025-04-14T10:41:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES } from '@ukef/constants';
+import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - error handling', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
+
+    counterpartyService.createMany = createCounterpartiesSpy;
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+    service.createInitialFacility = createInitialFacilitySpy;
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('when counterpartyService.createInitialFacility throws an error', () => {
path: src/modules/gift/gift.service-createFacility-error-handling.test.ts
user: login: christianmcc-ukef
body: typo here on counterpartyService - giftService
created_at: 2025-04-14T10:45:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES } from '@ukef/constants';
+import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - error handling', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
+
+    counterpartyService.createMany = createCounterpartiesSpy;
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+    service.createInitialFacility = createInitialFacilitySpy;
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('when counterpartyService.createInitialFacility throws an error', () => {
+    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
+
+    const mockAxiosErrorData = {
+      validationErrors: [{ mock: true }],
+    };
+
+    beforeEach(() => {
+      createInitialFacilitySpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
+
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+      service.createInitialFacility = createInitialFacilitySpy;
+    });
+
+    it('should throw an error', async () => {
+      const response = service.createFacility(mockPayload);
+
+      const expected = 'Error creating GIFT facility';
+
+      await expect(response).rejects.toThrow(expected);
+    });
+  });
+
+  describe('when counterpartyService.createMany throws an error', () => {
path: src/modules/gift/gift.service-createFacility-error-handling.test.ts
user: login: christianmcc-ukef
body: testing basically the same thing but should we add one for repaymentProfileService.createMany?
created_at: 2025-04-14T10:46:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { IsDefined, IsNumber, IsString, Length, Min } from 'class-validator';
+
+const {
+  GIFT: { REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: {
+    REPAYMENT_PROFILE: { ALLOCATION: VALIDATION },
+  },
+} = GIFT;
+
+/**
+ * GIFT repayment profile allocation DTO.
+ * These fields are required for APIM to create a repayment profile allocation in GIFT.
+ */
+export class GiftRepaymentProfileAllocationDto {
+  @IsDefined()
+  @IsNumber()
+  @Min(VALIDATION.AMOUNT.MIN)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().amount,
+  })
+  amount: number;
+
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.DUE_DATE.MIN_LENGTH, VALIDATION.DUE_DATE.MAX_LENGTH)
+  @ApiProperty({
+    example: REPAYMENT_PROFILE_ALLOCATION().dueDate,
+  })
+  dueDate: string;
path: src/modules/gift/dto/repayment-profile-allocation.ts
user: login: tony-barnes-ukef
body: I was thinking of surfacing such errors in APIM so that a call would be instantly rejected and *nothing* would be created in GIFT.

I agree some "big" business logic should be handled purely by GIFT as it could get out of sync. However this seems very unlikely to change - i.e, each payment date must be different.

What do you think @christianmcc-ukef ?
created_at: 2025-04-14T10:58:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Left a comment above 
created_at: 2025-04-14T10:58:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: tony-barnes-ukef
body: This is for the HTTP service though (`giftHttpService.post`), rather than the initial create response
created_at: 2025-04-14T13:00:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - bad requests', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: tony-barnes-ukef
body: Great spot
created_at: 2025-04-14T13:02:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,114 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES } from '@ukef/constants';
+import { mockAxiosError, mockResponse201 } from '@ukef-test/http-response';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
+
+const mockCounterparties = [COUNTERPARTY(), COUNTERPARTY(), COUNTERPARTY()];
+const mockRepaymentProfiles = [REPAYMENT_PROFILE(), REPAYMENT_PROFILE(), REPAYMENT_PROFILE()];
+
+const mockCreateCounterpartiesResponse = mockCounterparties.map((counterparty) => mockResponse201(counterparty));
+
+const mockRepaymentProfilesResponse = mockRepaymentProfiles.map((repaymentProfile) => mockResponse201(repaymentProfile));
+
+describe('GiftService.createFacility - error handling', () => {
+  let httpService: HttpService;
+  let counterpartyService: GiftCounterpartyService;
+  let repaymentProfileService: GiftRepaymentProfileService;
+  let service: GiftService;
+
+  let giftHttpService;
+  let mockHttpServicePost: jest.Mock;
+  let createInitialFacilitySpy: jest.Mock;
+  let createCounterpartiesSpy: jest.Mock;
+  let createRepaymentProfilesSpy: jest.Mock;
+
+  beforeEach(() => {
+    httpService = new HttpService();
+
+    mockHttpServicePost = jest.fn().mockResolvedValueOnce(mockResponsePost);
+
+    httpService.post = mockHttpServicePost;
+
+    giftHttpService = {
+      post: mockHttpServicePost,
+    };
+
+    counterpartyService = new GiftCounterpartyService(giftHttpService);
+    repaymentProfileService = new GiftRepaymentProfileService(giftHttpService);
+
+    createInitialFacilitySpy = jest.fn().mockResolvedValueOnce(mockResponse201(FACILITY_RESPONSE_DATA));
+    createCounterpartiesSpy = jest.fn().mockResolvedValueOnce(mockCreateCounterpartiesResponse);
+    createRepaymentProfilesSpy = jest.fn().mockResolvedValueOnce(mockRepaymentProfilesResponse);
+
+    counterpartyService.createMany = createCounterpartiesSpy;
+    repaymentProfileService.createMany = createRepaymentProfilesSpy;
+
+    service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+    service.createInitialFacility = createInitialFacilitySpy;
+  });
+
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('when counterpartyService.createInitialFacility throws an error', () => {
+    const mockAxiosErrorStatus = HttpStatus.BAD_REQUEST;
+
+    const mockAxiosErrorData = {
+      validationErrors: [{ mock: true }],
+    };
+
+    beforeEach(() => {
+      createInitialFacilitySpy = jest.fn().mockRejectedValueOnce(mockAxiosError({ data: mockAxiosErrorData, status: mockAxiosErrorStatus }));
+
+      service = new GiftService(giftHttpService, counterpartyService, repaymentProfileService);
+
+      service.createInitialFacility = createInitialFacilitySpy;
+    });
+
+    it('should throw an error', async () => {
+      const response = service.createFacility(mockPayload);
+
+      const expected = 'Error creating GIFT facility';
+
+      await expect(response).rejects.toThrow(expected);
+    });
+  });
+
+  describe('when counterpartyService.createMany throws an error', () => {
path: src/modules/gift/gift.service-createFacility-error-handling.test.ts
user: login: tony-barnes-ukef
body: Whoops, meant to add this in. Done
created_at: 2025-04-14T13:05:05Z
author_association: COLLABORATOR
diff_hunk: @@ -25,14 +31,39 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
path: src/constants/examples/gift.examples.constant.ts
user: login: tony-barnes-ukef
body: Would be good to split it up - can look into this in the next ticket, where a new entity will be added.

This is only used for swagger documentation and tests, but regardless I agree and would like to split up.

In the meantime, I've updated the amounts to be hard coded 👍 
created_at: 2025-04-14T13:09:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: abhi-markan
body: I would suggest having same validation in TFS to ensure an invalid payload is stopped in TFS rather than going all the way to GIFT API and then being terminated there due to a payload validation error.
created_at: 2025-04-14T14:04:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Agree @abhi-markan I said the same here: https://github.com/UK-Export-Finance/tfs-api/pull/976#discussion_r2041909509
created_at: 2025-04-14T14:11:17Z
author_association: COLLABORATOR
diff_hunk: @@ -25,14 +31,39 @@ const COUNTERPARTY = () => ({
   startDate: '2025-04-10',
 });
 
+/**
+ * Repayment profile allocation example.
+ * NOTE: The total of all amounts should not be greater than the facility amount.
+ * NOTE: Each due date is unique.
+ */
+const REPAYMENT_PROFILE_ALLOCATION = (index: number = 0) => {
path: src/constants/examples/gift.examples.constant.ts
user: login: christianmcc-ukef
body: Sounds good, thanks :)
created_at: 2025-04-14T14:51:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: christianmcc-ukef
body: Hmm interesting, it's definitely a delicate balance - validating more upfront means you have less clean up to do but I know that we've already have multiple changes to validations on the repayment profile amount side on what's allowed and I didn't want us to have to change more things in the future. I guess it's where you draw the line, initially I had thought it would be more on the format of the payload side rather than how fields depend on each other 

Personally I'd focus our efforts on making sure our clean-up process was tight and failing validations didn't have any negative impact in GIFT rather than adding in extra validations in TFS - because there are lots like what we're talking about here (e.g. dates being before/after each other)

with infinite time it would be nicer to export validations from GIFT in a package or something and use a subset of them in TFS to be able to fail fast but I don't think it's worth it right now 
created_at: 2025-04-14T14:57:28Z
author_association: NONE
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: christianmcc-ukef
body: Is the giftHttpService.post in gift.service not just being used for the creating the initial facility though? 
created_at: 2025-04-14T15:03:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Thanks @christianmcc-ukef Understood - I think we should aim for 0 clean up or try to prevent the need for such things as much as possible; with the continued goal of enabling straight through processing, with zero errors occurring. 

RE changing things, I expect things to changing during MVP development, but after MVP I would find it extremely unlikely for such business logic/validation rules to change (e.g with repayment names and dates having to be unique). Even if so it should be easy to update APIM TFS unless it's a "wild" change.

I agree it's not ideal to have, effectively some duplicated validation in APIM TFS and GIFT, but leading on from my previous sentence, I think it is worth having for at least important high level entity rules.

Would be great to have a DRY package for such validation rules that both APIM TFS, GIFT and potentially other NestJS services could use. One for the future perhaps.
created_at: 2025-04-14T15:28:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Another angle on this would be - if an entity has some fundamentally wrong data - such as conflicting payment dates, I don't think it should call GIFT at all, as it's just allowing invalid data to be passed and then clean up/human intervention etc, which we can prevent by including the validation 
created_at: 2025-04-14T15:31:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,318 @@
+import { HttpService } from '@nestjs/axios';
+import { HttpStatus } from '@nestjs/common';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { mockResponse201 } from '@ukef-test/http-response';
+import { AxiosResponse } from 'axios';
+
+import { GiftCounterpartyService } from './gift.counterparty.service';
+import { GiftRepaymentProfileService } from './gift.repayment-profile.service';
+import { GiftService } from './gift.service';
+import { mapAllValidationErrorResponses, mapValidationErrorResponses } from './helpers';
+
+const {
+  GIFT: { COUNTERPARTY, FACILITY_RESPONSE_DATA, FACILITY_CREATION_PAYLOAD: mockPayload, REPAYMENT_PROFILE },
+} = EXAMPLES;
+
+const { API_RESPONSE_MESSAGES, ENTITY_NAMES } = GIFT;
+
+const mockResponsePost = mockResponse201(EXAMPLES.GIFT.FACILITY_RESPONSE_DATA);
path: src/modules/gift/gift.service-createFacility-bad-requests.test.ts
user: login: tony-barnes-ukef
body: Sorry, I misread the mocks. `mockResponsePost` was used to mock a few things that are no longer necessary 😎 

Renamed to `mockCreateInitialFacilityResponse`.


created_at: 2025-04-14T15:40:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: christianmcc-ukef
body: Happy for this (and my other comment) to be resolved based off the above. We should organise a meeting on just clean-up etc./manual intervention etc. in general with BAs and probably Charles as well. I'm genuinely not sure what the steer will be but it'll want to be looked at a lot given the history with ACBS and it's poor handling :D 
created_at: 2025-04-14T16:01:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: abhi-markan
body: Thanks both, we also need to keep in mind APIM is also an API wrapper alongside with other services it provided. I would expect an API wrapper to validate the payload semanticaly.
created_at: 2025-04-15T08:16:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { EXAMPLES, GIFT } from '@ukef/constants';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsDefined, IsString, Length, ValidateNested } from 'class-validator';
+
+import { GiftRepaymentProfileAllocationDto } from './repayment-profile-allocation';
+
+const {
+  GIFT: { REPAYMENT_PROFILE, REPAYMENT_PROFILE_ALLOCATION },
+} = EXAMPLES;
+
+const {
+  VALIDATION: { REPAYMENT_PROFILE: VALIDATION },
+} = GIFT;
+
+/**
+ * GIFT repayment profile DTO.
+ * These fields are required for APIM to create a repayment profile in GIFT.
+ */
+export class GiftRepaymentProfileDto {
+  @IsDefined()
+  @IsString()
+  @Length(VALIDATION.NAME.MIN_LENGTH, VALIDATION.NAME.MAX_LENGTH)
+  @ApiProperty({
+    required: true,
+    example: REPAYMENT_PROFILE.name,
+  })
+  name: string;
path: src/modules/gift/dto/repayment-profile.ts
user: login: tony-barnes-ukef
body: Thanks @christianmcc-ukef - separate discussion for outside of this PR, but I agree we should have a meeting about such things.

Have spoken with Abhi about this before; We want to strive for 0 errors and therefore no clean up should be required, but if something does go wrong, a notification is sent to dev and a BA/business. Business can then fix/update the facility manually in GIFT UI in order to progress the real world deal, whilst dev can debug and prevent the issue from happening again in the future.

I hope we'll be able to catch 99% of potential troublesome fields/errors, when we start on the DTFS side of things.

Regardless, I've made a note to arrange a meeting about this once we start on the DTFS piece of work.
created_at: 2025-04-15T08:16:36Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-14T15:40:58Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=UK-Export-Finance_tfs-api&pullRequest=976) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=UK-Export-Finance_tfs-api&pullRequest=976&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=UK-Export-Finance_tfs-api&pullRequest=976&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=UK-Export-Finance_tfs-api&pullRequest=976&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=UK-Export-Finance_tfs-api&pullRequest=976&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [24.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=UK-Export-Finance_tfs-api&pullRequest=976&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=UK-Export-Finance_tfs-api&pullRequest=976)


author_association: NONE