Skip to content

Commit

Permalink
Merge pull request #28 from xeptagondev/main
Browse files Browse the repository at this point in the history
Phase 2 Release
reinaotsuka authored Aug 1, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 4011a1f + cc2ea6d commit d1c6100
Showing 650 changed files with 54,712 additions and 5,374 deletions.
156 changes: 156 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Carbon Transparency Test Deployment
on:
workflow_dispatch:
push:
branches:
- "staging"
paths:
- backend/**
- web/**
- .github/workflows/deployment*

env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1

jobs:
changes:
name: Carbon Transparency Deploy Pre
runs-on: ubuntu-latest
outputs:
backend-changes: ${{ steps.changes.outputs.backend-changes }}
workflows-changes: ${{ steps.changes.outputs.workflows-changes }}
frontend-changes: ${{ steps.changes.outputs.frontend-changes }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Determine changed services
id: changes
run: |
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
if echo "$CHANGED_FILES" | grep -q "backend/"; then
echo "Backend changes detected."
echo "backend-changes=true" >> $GITHUB_OUTPUT
else
echo "No Backend changes detected."
echo "backend-changes=false" >> $GITHUB_OUTPUT
fi
if echo echo "$CHANGED_FILES" | grep -q ".github/workflows/"; then
echo "Workflow changes detected."
echo "workflows-changes=true" >> $GITHUB_OUTPUT
else
echo "No Workflow changes detected."
echo "workflows-changes=false" >> $GITHUB_OUTPUT
fi
if echo "$CHANGED_FILES" | grep -q "web/"; then
echo "Frontend changes detected."
echo "frontend-changes=true" >> $GITHUB_OUTPUT
else
echo "No Frontend changes detected."
echo "frontend-changes=false" >> $GITHUB_OUTPUT
fi
backend-deploy:
needs: changes
if: needs.changes.outputs.backend-changes == 'true' || needs.changes.outputs.workflows-changes == 'true'
name: Carbon Transparency Backend Deploy
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-'backend'
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push the backend images to Amazon ECR
id: build-backend-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: transparency-services
IMAGE_TAG: ${{ github.head_ref || github.ref_name }}
run: |
# Build a docker container and push it to ECR
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f backend/services/Dockerfile .
echo "Pushing image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy backend images to Amazon EC2
id: deploy-backend
env:
PRIVATE_KEY: ${{ secrets.AWS_SSH_KEY_PRIVATE }}
HOSTNAME: ${{secrets.HOST_IP }}
USER_NAME: ec2-user
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} '
repos/carbon-transparency/mrv_backend_deploy.sh '
frontend-deploy:
needs: changes
if: needs.changes.outputs.frontend-changes == 'true' || needs.changes.outputs.workflows-changes == 'true'
name: Carbon Transparency Frontend Deploy
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-'web'
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push the frontend images to Amazon ECR
id: build-frontend-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: transparency-web
IMAGE_TAG: ${{ github.head_ref || github.ref_name }}
run: |
# Build a docker container and push it to ECR
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f web/Dockerfile . --build-arg PORT=3030 --build-arg REACT_APP_BACKEND=http://localhost:9000 --build-arg REACT_APP_STAT_URL=http://localhost:9100 --build-arg COUNTRY_NAME="CountryX" --build-arg COUNTRY_FLAG_URL="https://carbon-common-dev.s3.amazonaws.com/flag.png" --build-arg COUNTRY_CODE="NG" --build-arg REACT_APP_MAP_TYPE="Mapbox" --build-arg REACT_APP_GOVERNMENT_MINISTRY:"Ministry Of Environment" --build-arg REACT_APP_MAPBOXGL_ACCESS_TOKEN=${{ secrets.MAPBOXGL_ACCESS_TOKEN }}
echo "Pushing image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy frontend images to Amazon EC2
id: deploy-frontend
env:
PRIVATE_KEY: ${{ secrets.AWS_SSH_KEY_PRIVATE }}
HOSTNAME: ${{secrets.HOST_IP }}
USER_NAME: ec2-user
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} '
repos/carbon-transparency/mrv_frontend_deploy.sh '
101 changes: 0 additions & 101 deletions .github/workflows/frontend-deployment.yml

This file was deleted.

156 changes: 156 additions & 0 deletions .github/workflows/prod_deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Carbon Transparency Demo Deployment
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- backend/**
- web/**
- .github/workflows/deployment*

env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1

jobs:
changes:
name: Carbon Transparency Deploy Pre
runs-on: ubuntu-latest
outputs:
backend-changes: ${{ steps.changes.outputs.backend-changes }}
workflows-changes: ${{ steps.changes.outputs.workflows-changes }}
frontend-changes: ${{ steps.changes.outputs.frontend-changes }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Determine changed services
id: changes
run: |
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
if echo "$CHANGED_FILES" | grep -q "backend/"; then
echo "Backend changes detected."
echo "backend-changes=true" >> $GITHUB_OUTPUT
else
echo "No Backend changes detected."
echo "backend-changes=false" >> $GITHUB_OUTPUT
fi
if echo echo "$CHANGED_FILES" | grep -q ".github/workflows/"; then
echo "Workflow changes detected."
echo "workflows-changes=true" >> $GITHUB_OUTPUT
else
echo "No Workflow changes detected."
echo "workflows-changes=false" >> $GITHUB_OUTPUT
fi
if echo "$CHANGED_FILES" | grep -q "web/"; then
echo "Frontend changes detected."
echo "frontend-changes=true" >> $GITHUB_OUTPUT
else
echo "No Frontend changes detected."
echo "frontend-changes=false" >> $GITHUB_OUTPUT
fi
backend-deploy:
needs: changes
if: needs.changes.outputs.backend-changes == 'true' || needs.changes.outputs.workflows-changes == 'true'
name: Carbon Registry Backend Deploy
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-'backend'
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push the backend images to Amazon ECR
id: build-backend-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: transparency-services
IMAGE_TAG: ${{ github.head_ref || github.ref_name }}
run: |
# Build a docker container and push it to ECR
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f backend/services/Dockerfile .
echo "Pushing image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy backend images to Amazon EC2
id: deploy-backend
env:
PRIVATE_KEY: ${{ secrets.AWS_SSH_KEY_PRIVATE_DEMO }}
HOSTNAME: ${{secrets.HOST_IP_DEMO }}
USER_NAME: ec2-user
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} '
repos/carbon-transparency/prod_backend_deploy.sh '
frontend-deploy:
needs: changes
if: needs.changes.outputs.frontend-changes == 'true' || needs.changes.outputs.workflows-changes == 'true'
name: Carbon Registry Frontend Deploy
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-'web'
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push the frontend images to Amazon ECR
id: build-frontend-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: transparency-web
IMAGE_TAG: ${{ github.head_ref || github.ref_name }}
run: |
# Build a docker container and push it to ECR
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f web/Dockerfile . --build-arg PORT=3030 --build-arg REACT_APP_BACKEND=http://localhost:9000 --build-arg REACT_APP_STAT_URL=http://localhost:9100 --build-arg COUNTRY_NAME="CountryX" --build-arg COUNTRY_FLAG_URL="https://carbon-common-dev.s3.amazonaws.com/flag.png" --build-arg COUNTRY_CODE="NG" --build-arg REACT_APP_MAP_TYPE="Mapbox" --build-arg REACT_APP_GOVERNMENT_MINISTRY:"Ministry Of Environment" --build-arg REACT_APP_MAPBOXGL_ACCESS_TOKEN=${{ secrets.MAPBOXGL_ACCESS_TOKEN }}
echo "Pushing image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy frontend images to Amazon EC2
id: deploy-frontend
env:
PRIVATE_KEY: ${{ secrets.AWS_SSH_KEY_PRIVATE_DEMO }}
HOSTNAME: ${{secrets.HOST_IP_DEMO }}
USER_NAME: ec2-user
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} '
repos/carbon-transparency/prod_frontend_deploy.sh '
203 changes: 0 additions & 203 deletions .github/workflows/server-deployments.yml

This file was deleted.

33 changes: 0 additions & 33 deletions .github/workflows/service-lib-update.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test-service-build.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ on:
push:
branches:
- '**' # matches every branch
- '!develop'
- '!staging'
- '!main' # excludes master
paths:
- backend/**
4 changes: 2 additions & 2 deletions .github/workflows/test-web-build.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ on:
push:
branches:
- '**' # matches every branch
- '!develop'
- '!staging'
- '!main' # excludes master
paths:
- web/**
@@ -44,7 +44,7 @@ jobs:
IMAGE_TAG: ${{ github.head_ref || github.ref_name }}
run: |
# Build a docker container and push it to ECR
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f web/Dockerfile . --build-arg PORT=3030 --build-arg REACT_APP_BACKEND=http://localhost:9000 --build-arg REACT_APP_STAT_URL=http://localhost:9100 --build-arg COUNTRY_NAME="CountryX" --build-arg COUNTRY_FLAG_URL="https://carbon-common-dev.s3.amazonaws.com/flag.png" --build-arg COUNTRY_CODE="NG" --build-arg REACT_APP_MAP_TYPE="Mapbox" --build-arg REACT_APP_MAPBOXGL_ACCESS_TOKEN=${{ secrets.MAPBOXGL_ACCESS_TOKEN }}
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f web/Dockerfile . --build-arg PORT=3030 --build-arg REACT_APP_BACKEND=http://localhost:9000 --build-arg REACT_APP_STAT_URL=http://localhost:9100 --build-arg COUNTRY_NAME="CountryX" --build-arg COUNTRY_FLAG_URL="https://carbon-common-dev.s3.amazonaws.com/flag.png" --build-arg COUNTRY_CODE="NG" --build-arg REACT_APP_MAP_TYPE="Mapbox" --build-arg REACT_APP_GOVERNMENT_MINISTRY:"Ministry Of Environment" --build-arg REACT_APP_MAPBOXGL_ACCESS_TOKEN=${{ secrets.MAPBOXGL_ACCESS_TOKEN }}
echo "Pushing image to ECR..."
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
33 changes: 0 additions & 33 deletions .github/workflows/web-lib-update.yml

This file was deleted.

15 changes: 13 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
{
"cSpell.words": [
"antd",
"Appstore",
"centered",
"cmpt",
"donut",
"Kpis",
"Popconfirm",
"Sider"
"Popover",
"Popups",
"Sider",
"typeorm",
"UNNEST",
"unvalidate",
"vars"
],
"compile-hero.disable-compile-files-on-did-save-code": true
"compile-hero.disable-compile-files-on-did-save-code": true,
"cSpell.language": "en-GB"
}
4 changes: 2 additions & 2 deletions backend/services/Dockerfile
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ RUN yarn run sls:installProd
COPY ./backend/services .

ENV NODE_ENV production
RUN yarn add @nestjs/cli --dev && yarn run build && yarn remove @nestjs/cli && yarn cache clean
RUN yarn add @nestjs/cli@9.0.0 --dev && yarn run build && yarn remove @nestjs/cli && yarn cache clean

# Start the server using the production build
CMD [ "node", "dist/main.js" ]
CMD [ "node", "dist/main.js" ]
9 changes: 6 additions & 3 deletions backend/services/package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,10 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:create": "npx typeorm-ts-node-commonjs migration:create",
"migration:run": "npx typeorm-ts-node-commonjs migration:run -d src/typeorm.config.service.ts",
"migration:revert": "npx typeorm-ts-node-commonjs migration:revert -d src/typeorm.config.service.ts"
},
"dependencies": {
"@aws-sdk/client-qldb": "^3.433.0",
@@ -38,7 +41,6 @@
"@nestjs/swagger": "^6.1.3",
"@nestjs/typeorm": "^9.0.1",
"@undp/carbon-credit-calculator": "^1.1.1",
"@undp/carbon-services-lib": "0.0.289",
"@undp/serial-number-gen": "^1.0.0",
"aws-lambda": "^1.0.7",
"aws-serverless-express": "^3.4.0",
@@ -64,7 +66,8 @@
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typeorm": "^0.3.10",
"winston": "^3.8.2"
"winston": "^3.8.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@aws-sdk/client-qldb-session": "^3.391.0",
53 changes: 53 additions & 0 deletions backend/services/src/action/action.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { forwardRef, Module } from '@nestjs/common';
import { ActionService } from './action.service';
import { ConfigModule } from '@nestjs/config';
import configuration from '../configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from '../typeorm.config.service';
import { ActionEntity } from '../entities/action.entity';
import { KpiEntity } from '../entities/kpi.entity';
import { LogEntity } from '../entities/log.entity';
import { ProgrammeEntity } from '../entities/programme.entity';
import { ProjectEntity } from '../entities/project.entity';
import { AchievementEntity } from '../entities/achievement.entity';
import { ActivityEntity } from '../entities/activity.entity';
import { SupportEntity } from '../entities/support.entity';
import { UtilModule } from '../util/util.module';
import { FileHandlerModule } from '../file-handler/filehandler.module';
import { ValidationModule } from '../validation/validation.module';
import { ActionViewEntity } from '../entities/action.view.entity';
import { KpiModule } from '../kpi/kpi.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
imports: undefined,
}),
TypeOrmModule.forFeature([
ActionEntity,
KpiEntity,
LogEntity,
ProgrammeEntity,
ProjectEntity,
AchievementEntity,
ActivityEntity,
SupportEntity,
ActionViewEntity,
]),
UtilModule,
FileHandlerModule,
ValidationModule,
forwardRef(() => KpiModule)
],
providers: [
ActionService
],
exports: [ActionService],
})
export class ActionModule { }
1,016 changes: 1,016 additions & 0 deletions backend/services/src/action/action.service.spec.ts

Large diffs are not rendered by default.

670 changes: 670 additions & 0 deletions backend/services/src/action/action.service.ts

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions backend/services/src/activity/activity.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from '../configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from '../typeorm.config.service';
import { ActionEntity } from '../entities/action.entity';
import { KpiEntity } from '../entities/kpi.entity';
import { LogEntity } from '../entities/log.entity';
import { ProgrammeEntity } from '../entities/programme.entity';
import { ProjectEntity } from '../entities/project.entity';
import { AchievementEntity } from '../entities/achievement.entity';
import { ActivityEntity } from '../entities/activity.entity';
import { SupportEntity } from '../entities/support.entity';
import { UtilModule } from '../util/util.module';
import { FileHandlerModule } from '../file-handler/filehandler.module';
import { ValidationModule } from '../validation/validation.module';
import { ActionViewEntity } from '../entities/action.view.entity';
import { ActivityService } from './activity.service';
import { ActionModule } from 'src/action/action.module';
import { ProgrammeModule } from 'src/programme/programme.module';
import { ProjectModule } from 'src/project/project.module';
import { ProgrammeViewEntity } from 'src/entities/programme.view.entity';
import { KpiModule } from 'src/kpi/kpi.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
imports: undefined,
}),
TypeOrmModule.forFeature([
ActionEntity,
KpiEntity,
LogEntity,
ProgrammeEntity,
ProjectEntity,
AchievementEntity,
ActivityEntity,
SupportEntity,
ActionViewEntity,
ProgrammeViewEntity
]),
ActionModule,
ProgrammeModule,
ProjectModule,
UtilModule,
FileHandlerModule,
ValidationModule,
KpiModule
],
providers: [
ActivityService
],
exports: [ActivityService],
})
export class ActivityModule { }
995 changes: 995 additions & 0 deletions backend/services/src/activity/activity.service.spec.ts

Large diffs are not rendered by default.

1,468 changes: 1,468 additions & 0 deletions backend/services/src/activity/activity.service.ts

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions backend/services/src/analytics-api/analytics.api.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Controller,
UseGuards,
Get,
Param,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { AnalyticsService } from "./analytics.api.service";
import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";

@ApiTags("Analytics")
@ApiBearerAuth('api_key')
@Controller("analytics")
export class AnalyticsController {
constructor(
private analyticsService: AnalyticsService,
) {}

@UseGuards(JwtAuthGuard)
@Get('/actionsSummery')
getClimateActionChart() {
return this.analyticsService.getClimateActionChart();
}

@UseGuards(JwtAuthGuard)
@Get('/projectSummary')
getProjectSummaryChart() {
return this.analyticsService.getProjectSummaryChart();
}

@UseGuards(JwtAuthGuard)
@Get('/supportSummary')
getSupportChart() {
return this.analyticsService.getActivitiesSupported();
}

@UseGuards(JwtAuthGuard)
@Get('/supportFinanceSummary')
getSupportFinanceChart() {
return this.analyticsService.getActivitiesFinance();
}

@UseGuards(JwtAuthGuard)
@Get('/ghgMitigationSummaryForYear/:year')
getGhgMitigationForYear(@Param('year') year: number) {
return this.analyticsService.getGhgMitigationForYear(year);
}

@UseGuards(JwtAuthGuard)
@Get('/getGhgMitigationSummary')
getGhgMitigationForRecentYear() {
return this.analyticsService.getGhgMitigationForRecentYear();
}
}
35 changes: 35 additions & 0 deletions backend/services/src/analytics-api/analytics.api.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Logger, Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import configuration from "../configuration";
import { Organisation } from "../entities/organisation.entity";
import { TypeOrmConfigService } from "../typeorm.config.service";
import { AnalyticsController } from "./analytics.api.controller";
import { AnalyticsService } from "./analytics.api.service";
import { AuthModule } from "../auth/auth.module";
import { CaslModule } from "../casl/casl.module";
import { ActivityEntity } from "../entities/activity.entity";
import { UtilModule } from "../util/util.module";

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
imports: undefined,
}),
TypeOrmModule.forFeature([
ActivityEntity
]),
AuthModule,
CaslModule,
UtilModule
],
controllers: [AnalyticsController],
providers: [Logger, AnalyticsService],
})
export class AnalyticsAPIModule { }
241 changes: 241 additions & 0 deletions backend/services/src/analytics-api/analytics.api.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { DataCountResponseDto } from "../dtos/data.count.response";
import { ActionEntity } from "../entities/action.entity";
import { EntityManager, Repository } from 'typeorm';
import { InjectEntityManager, InjectRepository } from "@nestjs/typeorm";
import { ProjectEntity } from "../entities/project.entity";
import { ActivityEntity } from "../entities/activity.entity";
import { FinanceNature, SupportDirection } from "../enums/support.enum";
import { HelperService } from "../util/helpers.service";

@Injectable()
export class AnalyticsService {

constructor(
@InjectEntityManager() private entityManager: EntityManager,
@InjectRepository(ActivityEntity) private activityRepo: Repository<ActivityEntity>,
private helperService: HelperService
) { }

async getClimateActionChart(): Promise<DataCountResponseDto> {
try {
const queryBuilder = this.entityManager.createQueryBuilder()
.select('sector, COUNT("actionId") as count, MAX(action.updatedTime) as "latestTime"')
.from(ActionEntity, 'action')
.groupBy('sector')
.orderBy('MAX(action.updatedTime)', 'DESC');

const result = await queryBuilder.getRawMany();

// Extract sectors and counts into separate arrays
const sectors = result.map(row => row.sector);
const counts = result.map(row => row.count);

// Get the latest time from the first row if result is not empty
const latestTime = result.length ? new Date(result[0].latestTime) : null;

// Convert latestTime to epoch if it's not null
const latestEpoch = latestTime ? Math.floor(latestTime.getTime() / 1000) : 0;

return new DataCountResponseDto({ sectors, counts }, latestEpoch);
} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}

}

async getProjectSummaryChart(): Promise<DataCountResponseDto> {
try {
const queryBuilder = this.entityManager.createQueryBuilder()
.select('sector, COUNT("projectId") as count, MAX(project.updatedTime) as "latestTime"')
.from(ProjectEntity, 'project')
.groupBy('sector')
.orderBy('MAX(project.updatedTime)', 'DESC');

const result = await queryBuilder.getRawMany();

// Extract sectors and counts into separate arrays
const sectors = result.map(row => row.sector);
const counts = result.map(row => row.count);

// Get the latest time from the first row if result is not empty
const latestTime = result.length ? new Date(result[0].latestTime) : null;

// Convert latestTime to epoch if it's not null
const latestEpoch = latestTime ? Math.floor(latestTime.getTime() / 1000) : 0;


return new DataCountResponseDto({ sectors, counts }, latestEpoch);
} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

async getActivitiesSupported() {
try {
const results = await this.activityRepo.createQueryBuilder('activity')
.leftJoin('activity.support', 'support')
.select([
'COUNT(DISTINCT activity.activityId) as "totalActivities"',
'COUNT(DISTINCT CASE WHEN support.financeNature = :financeNature AND support.direction = :directionReceived THEN activity.activityId END) as "supportReceivedActivities"',
'GREATEST(MAX(activity."updatedTime"), MAX(support."updatedTime")) as "latestTime"'
])
.setParameter('financeNature', FinanceNature.INTERNATIONAL)
.setParameter('directionReceived', SupportDirection.RECEIVED)
.getRawOne();

const totalActivities = results.totalActivities ? parseInt(results.totalActivities) : 0;
const supportReceivedActivities = results.supportReceivedActivities ? parseInt(results.supportReceivedActivities) : 0;
const supportNeededActivities = totalActivities - supportReceivedActivities;

const latestTime = results.latestTime ? new Date(results.latestTime).getTime() / 1000 : 0;

return new DataCountResponseDto({ supportReceivedActivities, supportNeededActivities }, latestTime);
} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

async getActivitiesFinance() {
try {
const results = await this.activityRepo.createQueryBuilder('activity')
.leftJoin('activity.support', 'support')
.select([
'sum(support."receivedAmount") as "supportReceived"', 'sum(support."requiredAmount") as "supportNeeded"',
'GREATEST(MAX(activity."updatedTime"), MAX(support."updatedTime")) as "latestTime"'
])
.getRawOne();

const supportReceived = results.supportReceived ? parseFloat(results.supportReceived) : 0;
const supportNeeded = results.supportNeeded ? parseFloat(results.supportNeeded) : 0;

const latestTime = results.latestTime ? new Date(results.latestTime).getTime() / 1000 : 0;

return new DataCountResponseDto({ supportReceived, supportNeeded }, latestTime);

} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

async getGhgMitigationForYear(year: number) {
try {
const query = `
SELECT
activity.sector,
SUM((activity."mitigationTimeline"->'expected'->'expectedEmissionReductWithM'->>(${year} - (activity."mitigationTimeline" ->> 'startYear')::int))::numeric) AS total,
Max(activity."updatedTime") as "latestTime"
FROM
activity
WHERE activity."mitigationTimeline" IS NOT NULL
AND (activity."mitigationTimeline" ->> 'startYear')::numeric <= ${year}
GROUP BY
activity.sector
HAVING
SUM((activity."mitigationTimeline" -> 'expected' -> 'expectedEmissionReductWithM' ->> (${year} - (activity."mitigationTimeline" ->> 'startYear')::int))::numeric) != 0
ORDER BY
"latestTime" DESC;
`;

const result = await this.entityManager.query(query);
// Extract sectors and counts into separate arrays
const sectors = result.map(row => row.sector);
const totals = result.map(row => row.total);

// Get the latest time from the first row if result is not empty
const latestTime = result.length ? new Date(result[0].latestTime) : null;

// Convert latestTime to epoch if it's not null
const latestEpoch = latestTime ? Math.floor(latestTime.getTime() / 1000) : 0;

return new DataCountResponseDto({ sectors, totals }, latestEpoch);
} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

async getGhgMitigationForRecentYear() {

// Get the current year
const currentYear = new Date().getFullYear();

// Calculate the previous year
const previousYear = currentYear - 1;

try {
const query = `
SELECT
activity.sector,
SUM((activity."mitigationTimeline"->'actual'->'actualEmissionReduct'->>(${previousYear} - (activity."mitigationTimeline" ->> 'startYear')::int))::numeric) AS total,
Max(activity."updatedTime") as "latestTime"
FROM
activity
WHERE activity."mitigationTimeline" IS NOT NULL
AND (activity."mitigationTimeline" ->> 'startYear')::numeric <= ${previousYear}
GROUP BY
activity.sector
HAVING
SUM((activity."mitigationTimeline" -> 'actual' -> 'actualEmissionReduct' ->> (${previousYear} - (activity."mitigationTimeline" ->> 'startYear')::int))::numeric) != 0
ORDER BY
"latestTime" DESC;
`;

const result = await this.entityManager.query(query);
const sectors = result.map(row => row.sector);
const totals = result.map(row => row.total);

// Get the latest time from the first row if result is not empty
const latestTime = result.length ? new Date(result[0].latestTime) : null;

// Convert latestTime to epoch if it's not null
const latestEpoch = latestTime ? Math.floor(latestTime.getTime() / 1000) : 0;

return new DataCountResponseDto({ sectors, totals }, latestEpoch);
} catch (err) {
console.log(err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.unableToGetStats",
[]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

}
3 changes: 2 additions & 1 deletion backend/services/src/analytics-api/handler.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,8 @@
import { Handler, Context } from "aws-lambda";
import { Server } from "http";
import { proxy } from "aws-serverless-express";
import { AnalyticsAPIModule, bootstrapServer } from "@undp/carbon-services-lib";
import { bootstrapServer } from "../server";
import { AnalyticsAPIModule } from "./analytics.api.module";

let cachedServer: Server;

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AsyncOperationsHandlerInterface } from './async-operations-handler-interface.service';
import { AsyncOperationsHandlerService } from './async-operations-handler.service';
import { AsyncActionEntity } from '../entities/async.action.entity';
import { Counter } from '../entities/counter.entity';
import { CounterType } from '../enums/counter.type.enum';

@Injectable()
export class AsyncOperationsDatabaseHandlerService
implements AsyncOperationsHandlerInterface
{
constructor(
private logger: Logger,
@InjectRepository(Counter) private counterRepo: Repository<Counter>,
@InjectRepository(AsyncActionEntity)
private asyncActionRepo: Repository<AsyncActionEntity>,
private asyncOperationsHandlerService: AsyncOperationsHandlerService,
) {}

async asyncHandler(event: any): Promise<any> {
this.logger.log('database asyncHandler started', JSON.stringify(event));

const seqObj = await this.counterRepo.findOneBy({
id: CounterType.ASYNC_OPERATIONS,
});
let lastSeq = 0;
if (seqObj) {
lastSeq = seqObj.counter;
}
let retryCount = 0;
const retryLimit = 50;
const baseDelay = 5000; // Initial delay for exponential backoff

const doActions = async () => {
console.log('lastSeq', lastSeq, 'retryCount', retryCount);
const notExecutedActions = await this.asyncActionRepo
.createQueryBuilder('asyncAction')
.where('asyncAction.actionId > :lastExecuted', { lastExecuted: lastSeq })
.orderBy('"actionId"', 'ASC')
.select(['"actionId"', '"actionType"', '"actionProps"'])
.getRawMany();

if (notExecutedActions.length !== 0) {
try {
for (const action of notExecutedActions) {
console.log('Action start', action.actionType, action.actionId);
await this.asyncOperationsHandlerService.handler(
action.actionType,
JSON.parse(action.actionProps),
);
lastSeq = action.actionId;
await this.counterRepo.save({
id: CounterType.ASYNC_OPERATIONS,
counter: lastSeq,
});
retryCount = 0; // Reset retry count after a successful execution
}
} catch (exception) {
this.logger.log('database asyncHandler failed', exception);
if (retryCount >= retryLimit) {
this.logger.log('database asyncHandler terminated');
return;
}
const delay = baseDelay * Math.pow(2, retryCount); // Exponential backoff
console.log(`Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
retryCount++;
return doActions(); // Retry the operation
}
}
// Schedule the next execution after a fixed delay regardless of success or failure
setTimeout(doActions, baseDelay);
};

await doActions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Injectable } from "@nestjs/common";

@Injectable()
export abstract class AsyncOperationsHandlerInterface {
abstract asyncHandler(event): Promise<any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable, Logger } from "@nestjs/common";
import { EmailService } from "../email/email.service";
import { AsyncActionType } from "../enums/async.action.type.enum";
// import { RegistryClientService } from "../shared/registry-client/registry-client.service";
// import { CadtApiService } from "../shared/cadt/cadt.api.service";

@Injectable()
export class AsyncOperationsHandlerService {
constructor(
private emailService: EmailService,
// private registryClient: RegistryClientService,
// private cadtService: CadtApiService,
private logger: Logger
) {}

async handler(actionType: any, dataObject: any) {

this.logger.log("AsyncOperationsHandlerService started", actionType.toString());
if (actionType) {
switch (actionType.toString()) {
case AsyncActionType.Email.toString():
return await this.emailService.sendEmail(dataObject);
// case AsyncActionType.RegistryCompanyCreate.toString():
// return await this.registryClient.createCompany(dataObject);
// case AsyncActionType.CompanyUpdate.toString():
// return await this.registryClient.CompanyUpdate(dataObject);
// case AsyncActionType.AuthProgramme.toString():
// return await this.registryClient.authProgramme(dataObject);
// case AsyncActionType.IssueCredit.toString():
// return await this.registryClient.issueCredit(dataObject);
// case AsyncActionType.RejectProgramme.toString():
// return await this.registryClient.rejectProgramme(dataObject);

// case AsyncActionType.ProgrammeCreate.toString():
// return this.registryClient.createProgramme(dataObject);
// case AsyncActionType.ProgrammeAccept.toString():
// return this.registryClient.programmeAccept(dataObject);
// case AsyncActionType.DocumentUpload.toString():
// return this.registryClient.addDocument(dataObject);
// case AsyncActionType.OwnershipUpdate.toString():
// return this.registryClient.updateOwnership(dataObject);
// case AsyncActionType.AddMitigation.toString():
// return this.registryClient.addMitigation(dataObject);
// case AsyncActionType.NationalInvestment.toString():
// return this.registryClient.addNationalInvestment(dataObject)

// case AsyncActionType.CADTProgrammeCreate.toString():
// return this.cadtService.createProgramme(dataObject)
// case AsyncActionType.CADTUpdateProgramme.toString():
// return this.cadtService.updateProgramme(dataObject.programme);
// case AsyncActionType.CADTCreditIssue.toString():
// return this.cadtService.issueCredit(dataObject.programme, dataObject.amount);
// case AsyncActionType.CADTTransferCredit.toString():
// return this.cadtService.transferCredit(dataObject.programme, dataObject.transfer);

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable, Logger } from "@nestjs/common";
import { AsyncOperationsHandlerInterface } from "./async-operations-handler-interface.service";
import { SQSEvent } from "aws-lambda";
import { AsyncOperationsHandlerService } from "./async-operations-handler.service";
import { AsyncActionType } from "../enums/async.action.type.enum";

type Response = { batchItemFailures: { itemIdentifier: string }[] };

@Injectable()
export class AsyncOperationsQueueHandlerService
implements AsyncOperationsHandlerInterface
{
constructor(
private asyncOperationsHandlerService: AsyncOperationsHandlerService,
private logger: Logger
) {}

async asyncHandler(event: SQSEvent): Promise<Response> {
this.logger.log("Queue asyncHandler started");
const response: Response = { batchItemFailures: [] };

for (const record of event.Records) {
const actionType = record.messageAttributes?.actionType?.stringValue;
try {

await this.asyncOperationsHandlerService.handler(
actionType,
JSON.parse(record.body)
);
} catch (exception) {
this.logger.log("queue asyncHandler failed", exception);
if (actionType?.toString() === AsyncActionType.Email.toString()) {
response.batchItemFailures.push({ itemIdentifier: record.messageId });
} else {
throw exception;
}
}
}
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Logger, Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AsyncOperationsDatabaseHandlerService } from "./async-operations-database-handler.service";
import { AsyncOperationsHandlerInterface } from "./async-operations-handler-interface.service";
import { AsyncOperationsQueueHandlerService } from "./async-operations-queue-handler.service";
import { AsyncOperationsHandlerService } from "./async-operations-handler.service";
import configuration from "../configuration";
import { EmailModule } from "../email/email.module";
import { AsyncActionEntity } from "../entities/async.action.entity";
import { Counter } from "../entities/counter.entity";
import { AsyncOperationType } from "../enums/async.operation.type.enum";
import { TypeOrmConfigService } from "../typeorm.config.service";


@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
}),
TypeOrmModule.forFeature([AsyncActionEntity, Counter]),
// RegistryClientModule,
EmailModule,
// CadtModule,
],
providers: [
{
provide: AsyncOperationsHandlerInterface,
useClass:
process.env.ASYNC_OPERATIONS_TYPE === AsyncOperationType.Queue
? AsyncOperationsQueueHandlerService
: AsyncOperationsDatabaseHandlerService,
},
Logger,
AsyncOperationsHandlerService,
],
})
export class AsyncOperationsModuleMain {}
6 changes: 3 additions & 3 deletions backend/services/src/async-operations-handler/handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NestFactory } from "@nestjs/core";
import { Handler, Context } from "aws-lambda";
import { getLogger } from "@undp/carbon-services-lib";
import { AsyncOperationsHandlerInterface } from "@undp/carbon-services-lib";
import { AsyncOperationsModuleMain } from "@undp/carbon-services-lib";
import { getLogger } from "../server";
import { AsyncOperationsModuleMain } from "./async-operations.module";
import { AsyncOperationsHandlerInterface } from "./async-operations-handler-interface.service";

export const handler: Handler = async (event: any, context: Context) => {
const app = await NestFactory.createApplicationContext(
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AsyncActionEntity } from "../entities/async.action.entity";
import { AsyncActionType } from "../enums/async.action.type.enum";
import { HelperService } from "../util/helpers.service";
import {
AsyncAction,
AsyncOperationsInterface,
} from "./async-operations.interface";

@Injectable()
export class AsyncOperationsDatabaseService
implements AsyncOperationsInterface
{
private emailDisabled: boolean;

constructor(
private configService: ConfigService,
private logger: Logger,
@InjectRepository(AsyncActionEntity)
private asyncActionRepo: Repository<AsyncActionEntity>,
private helperService: HelperService
) {
this.emailDisabled = this.configService.get<boolean>("email.disabled");
}

public tx:AsyncAction[]=[]

public async flushTx(): Promise<boolean>{
for (var action of this.tx){
//execute action
}
this.tx=[]
return true
}

public async AddAction(action: AsyncAction): Promise<boolean> {

if ([AsyncActionType.DocumentUpload, AsyncActionType.IssueCredit, AsyncActionType.RegistryCompanyCreate, AsyncActionType.RejectProgramme,AsyncActionType.AddMitigation,AsyncActionType.ProgrammeAccept,AsyncActionType.ProgrammeCreate,AsyncActionType.OwnershipUpdate, AsyncActionType.CompanyUpdate].includes(action.actionType) && !this.configService.get("registry.syncEnable")) {
this.logger.log(`Dropping sync event ${action.actionType} due to sync disabled`)
return false;
}

if (action.actionType === AsyncActionType.Email) {
if (this.emailDisabled) return false;
}

let asyncActionEntity: AsyncActionEntity = {} as AsyncActionEntity;
asyncActionEntity.actionType = action.actionType;
asyncActionEntity.actionProps = JSON.stringify(action.actionProps);
await this.asyncActionRepo.save(asyncActionEntity).catch((err: any) => {
this.logger.error("error", err);
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.addAsyncActionDatabaseFailed",
["Email"]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
});

this.logger.log("Successfully added to the AsyncAction table", action);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AsyncActionType } from "../enums/async.action.type.enum";
import { HelperService } from "../util/helpers.service";
import {
AsyncAction,
AsyncOperationsInterface,
} from "./async-operations.interface";

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"

@Injectable()
export class AsyncOperationsQueueService implements AsyncOperationsInterface {
private emailDisabled: boolean;
private sqs = new SQSClient({});

constructor(
private configService: ConfigService,
private logger: Logger,
private helperService: HelperService
) {
this.emailDisabled = this.configService.get<boolean>("email.disabled");
}
public tx:AsyncAction[]=[]

public async flushTx(): Promise<boolean>{
for (var action of this.tx){
//execute action
}
this.tx=[]
return true
}

public async AddAction(action: AsyncAction): Promise<boolean> {
// var params = {};

if ([AsyncActionType.DocumentUpload, AsyncActionType.IssueCredit, AsyncActionType.RegistryCompanyCreate, AsyncActionType.RejectProgramme,AsyncActionType.AddMitigation,AsyncActionType.ProgrammeAccept,AsyncActionType.ProgrammeCreate,AsyncActionType.OwnershipUpdate].includes(action.actionType) && !this.configService.get("registry.syncEnable")) {
this.logger.log(`Dropping sync event ${action.actionType} due to sync disabled`)
return false;
}

if (action.actionType === AsyncActionType.Email) {
if (this.emailDisabled) {
return false;
}
}

if ([AsyncActionType.CADTProgrammeCreate, AsyncActionType.CADTCertify, AsyncActionType.CADTCreditIssue, AsyncActionType.CADTTransferCredit, AsyncActionType.CADTUpdateProgramme].includes(action.actionType) && !this.configService.get('cadTrust.enable')) {
return false;
}

// params = {
// MessageAttributes: {
// actionType: {
// DataType: "Number",
// StringValue: action.actionType.toString(),
// },
// },
// MessageBody: JSON.stringify(action.actionProps),
// MessageGroupId: action.actionType.toString() + new Date().getTime(),
// QueueUrl: this.configService.get("asyncQueueName"),
// };

const params = new SendMessageCommand({
QueueUrl: this.configService.get("asyncQueueName"),
MessageAttributes: {
actionType: {
DataType: "Number",
StringValue: action.actionType.toString(),
},
},
MessageBody: JSON.stringify(action.actionProps),
MessageGroupId: this.configService.get("systemType")
});

try {
await this.sqs.send(params);
this.logger.log("Successfully added to the queue", action.actionType);
} catch (error) {
this.logger.error("Failed when adding to queue", action.actionType);
this.logger.error("Error",error)
throw new HttpException(
this.helperService.formatReqMessagesString(
"common.addAsyncActionQueueFailed",
["Email"]
),
HttpStatus.INTERNAL_SERVER_ERROR
);
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Injectable } from "@nestjs/common";

export interface AsyncAction {
actionType: number;
actionProps: any;
}

@Injectable()
export abstract class AsyncOperationsInterface {
public abstract tx:AsyncAction[]
public abstract flushTx(): Promise<boolean>;
public abstract AddAction(action: AsyncAction): Promise<boolean>;
}
38 changes: 38 additions & 0 deletions backend/services/src/async-operations/async-operations.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { forwardRef, Logger, Module } from "@nestjs/common";
import { AsyncOperationsInterface } from "./async-operations.interface";
import { AsyncOperationsQueueService } from "./async-operations-queue.service";
import configuration from "../configuration";
import { ConfigModule } from "@nestjs/config";
import { AsyncOperationType } from "../enums/async.operation.type.enum";
import { AsyncOperationsDatabaseService } from "./async-operations-database.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AsyncActionEntity } from "../entities/async.action.entity";
import { TypeOrmConfigService } from "../typeorm.config.service";
import { UtilModule } from "../util/util.module";

@Module({
exports: [AsyncOperationsInterface],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
}),
TypeOrmModule.forFeature([AsyncActionEntity]),
forwardRef(() => UtilModule)
],
providers: [
Logger,
{
provide: AsyncOperationsInterface,
useClass:
process.env.ASYNC_OPERATIONS_TYPE === AsyncOperationType.Queue
? AsyncOperationsQueueService
: AsyncOperationsDatabaseService,
}
]
})
export class AsyncOperationsModule {}
44 changes: 44 additions & 0 deletions backend/services/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Logger, Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./strategies/jwt.strategy";
import { LocalStrategy } from "./strategies/local.strategy";
import { CaslModule } from "../casl/casl.module";
import { ApiKeyStrategy } from "./strategies/apikey.strategy";
import { UserModule } from "../user/user.module";
import { UtilModule } from "../util/util.module";
import { AsyncOperationsModule } from "../async-operations/async-operations.module";
import { PasswordReset } from "../entities/userPasswordResetToken.entity";


@Module({
imports: [
UserModule,
PassportModule,
UtilModule,
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => ({
secretOrPrivateKey: configService.get<string>("jwt.userSecret"),
signOptions: {
expiresIn: parseInt(configService.get<string>("jwt.expiresIn")),
},
}),
inject: [ConfigService],
imports: undefined,
}),
CaslModule,
AsyncOperationsModule,
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
ApiKeyStrategy,
Logger,
PasswordReset,
],
exports: [AuthService],
})
export class AuthModule {}
18 changes: 18 additions & 0 deletions backend/services/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();

service = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
158 changes: 158 additions & 0 deletions backend/services/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { instanceToPlain } from "class-transformer";
import { CaslAbilityFactory } from "../casl/casl-ability.factory";
import { UserService } from "../user/user.service";
import { ConfigService } from "@nestjs/config";
import { PasswordResetService } from "../util/passwordReset.service";
import { AsyncAction, AsyncOperationsInterface } from "../async-operations/async-operations.interface";
import { PasswordHashService } from "../util/passwordHash.service";
import { AsyncActionType } from "../enums/async.action.type.enum";
import { BasicResponseDto } from "../dtos/basic.response.dto";
import { HelperService } from "../util/helpers.service";
import { JWTPayload } from "../dtos/jwt.payload";
import { EmailTemplates } from "../email-helper/email.template";
import { API_KEY_SEPARATOR } from "../constants";
import { UserState } from "../enums/user.enum";
import { User } from "../entities/user.entity";

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private configService: ConfigService,
private helperService: HelperService,
private passwordReset: PasswordResetService,
public caslAbilityFactory: CaslAbilityFactory,
private asyncOperationsInterface: AsyncOperationsInterface,
private passwordHashService: PasswordHashService
) {}

async validateUser(username: string, pass: string): Promise<any> {
let validationResponse : {user: Omit<User, 'password'> | null; message: string};

pass = this.passwordHashService.getPasswordHash(pass);
const user = await this.userService.getUserCredentials(username?.toLowerCase());

if (user) {
if (user.state == UserState.ACTIVE) {
if (user.password === pass) {
const { password, ...result } = user;
validationResponse = { user: result, message: 'Validated'};
} else {
validationResponse = {user : null, message: "incorrectPassword"}
}
} else {
validationResponse = {user : null, message: "accountDeactivated"}
}
} else {
validationResponse = {user : null, message: "invalidUsername"}
}

return validationResponse;
}

async validateApiKey(apiKey: string): Promise<any> {
const parts = Buffer.from(apiKey, "base64")
.toString("utf-8")
.split(API_KEY_SEPARATOR);
if (parts.length != 2) {
return null;
}
const user = await this.userService.getUserCredentials(parts[0]?.toLowerCase());
if (user && user.apiKey === apiKey) {
const { password, apiKey, ...result } = user;
return result;
}
return null;
}

async login(user: any) {
const payload = new JWTPayload(
user.organisation,
user.name,
user.id,
user.role,
user.subRole,
user.sector,
user.email,
user.validatePermission,
user.subRolePermission,
user.ghgInventoryPermission,
);
const ability = this.caslAbilityFactory.createForUser(user);
return {
access_token: this.jwtService.sign(instanceToPlain(payload)),
role: user.role,
subRole: user.subRole,
id: user.id,
name: user.name,
company: user.organisation,
ability: JSON.stringify(ability),
sector: user.sector,
userState: user.state,
validatePermission: user.validatePermission,
subRolePermission: user.subRolePermission,
ghgInventoryPermission: user.ghgInventoryPermission,
};
}

async forgotPassword(email: any) {
const hostAddress = this.configService.get("host");
const userDetails = await this.userService.findOne(email);
if (userDetails && userDetails.state == UserState.ACTIVE) {
console.table(userDetails);
const requestId = this.helperService.generateRandomPassword();
const date = Date.now();
const expireDate = date + 3600 * 1000; // 1 hout expire time
const passwordResetD = {
email: email,
token: requestId,
expireTime: expireDate,
};
await this.passwordReset.deletePasswordResetD(email);
await this.passwordReset.insertPasswordResetD(passwordResetD);

const templateData = {
name: userDetails.name,
requestId: requestId,
home: hostAddress,
countryName: this.configService.get("systemCountryName"),
};

const action: AsyncAction = {
actionType: AsyncActionType.Email,
actionProps: {
emailType: EmailTemplates.FORGOT_PASSOWRD.id,
sender: email,
subject: this.helperService.getEmailTemplateMessage(
EmailTemplates.FORGOT_PASSOWRD["subject"],
templateData,
true
),
emailBody: this.helperService.getEmailTemplateMessage(
EmailTemplates.FORGOT_PASSOWRD["html"],
templateData,
false
),
},
};

await this.asyncOperationsInterface.AddAction(action);

return new BasicResponseDto(
HttpStatus.OK,
this.helperService.formatReqMessagesString("user.resetEmailSent", [])
);
} else {
throw new HttpException(
this.helperService.formatReqMessagesString(
"user.forgotPwdUserNotFound",
[]
),
HttpStatus.NOT_FOUND
);
}
}
}
7 changes: 7 additions & 0 deletions backend/services/src/auth/guards/api-jwt-key.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class ApiKeyJwtAuthGuard extends AuthGuard(["jwt", "api-key"]) {

}
5 changes: 5 additions & 0 deletions backend/services/src/auth/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
5 changes: 5 additions & 0 deletions backend/services/src/auth/guards/local-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
20 changes: 20 additions & 0 deletions backend/services/src/auth/strategies/apikey.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { AuthService } from '../auth.service';

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 'api-key') {
constructor(private authService: AuthService) {
const headerKeyApiKey = 'api_key';

super({ header: headerKeyApiKey, prefix: '' }, true, async (apiKey, done) => {
const user = this.authService.validateApiKey(apiKey)
if (user) {
done(null, user);
}
done(new UnauthorizedException(), null);
});
}
}
30 changes: 30 additions & 0 deletions backend/services/src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JWTPayload } from '../../dtos/jwt.payload';
import { UserState } from 'src/enums/user.enum';
import { UserService } from 'src/user/user.service';


@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService, private logger: Logger, private readonly userService: UserService,) {
const secret = configService.get<string>('jwt.userSecret');
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}

async validate(payload: any) {
const jwtPayload: JWTPayload = plainToClass(JWTPayload, payload)
const user = await this.userService.getUserCredentials(jwtPayload.un);
if (user.state !== UserState.ACTIVE) {
throw new UnauthorizedException('user deactivated');
}
return { id: user.id, companyName: user.organisation, role: user.role, subRole: user.subRole, name: user.name, sector: user.sector, validatePermission: user.validatePermission, subRolePermission:user.subRolePermission, ghgInventoryPermission:user.ghgInventoryPermission };
}
}
20 changes: 20 additions & 0 deletions backend/services/src/auth/strategies/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { User } from 'src/entities/user.entity';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}

async validate(username: string, password: string): Promise<any> {
const validationResponse: {user: Omit<User, 'password'> | null; message: string} = await this.authService.validateUser(username, password);
if (!validationResponse.user) {
throw new UnauthorizedException();
}
return validationResponse.user;
}
}
11 changes: 11 additions & 0 deletions backend/services/src/casl/action.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
Approve = 'approve',
Reject = 'reject',
Validate = 'validate',
ForceResetPassword = 'forceResetPassword',
}
7 changes: 7 additions & 0 deletions backend/services/src/casl/casl-ability.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CaslAbilityFactory } from './casl-ability.factory';

describe('CaslAbilityFactory', () => {
it('should be defined', () => {
expect(new CaslAbilityFactory()).toBeDefined();
});
});
408 changes: 408 additions & 0 deletions backend/services/src/casl/casl-ability.factory.ts

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions backend/services/src/casl/casl.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { CaslAbilityFactory } from "./casl-ability.factory";
import { UtilModule } from "../util/util.module";

@Module({
imports: [UtilModule],
providers: [CaslAbilityFactory],
exports: [CaslAbilityFactory],
})
export class CaslModule {}
6 changes: 6 additions & 0 deletions backend/services/src/casl/policy.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SetMetadata } from "@nestjs/common";
import { PolicyHandler } from "./policy.handler";

export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
SetMetadata(CHECK_POLICIES_KEY, handlers);
216 changes: 216 additions & 0 deletions backend/services/src/casl/policy.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
mixin,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { plainToClass } from "class-transformer";
import { Stat } from "../dtos/stat.dto";
import { EntitySubject } from "../entities/entity.subject";
import { User } from "../entities/user.entity";
import { Action } from "./action.enum";
import { CaslAbilityFactory, AppAbility } from "./casl-ability.factory";
import { CHECK_POLICIES_KEY } from "./policy.decorator";
import { PolicyHandler } from "./policy.handler";
import { HelperService } from "../util/helpers.service";
const { rulesToQuery } = require("@casl/ability/extra");

@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
public reflector: Reflector,
public caslAbilityFactory: CaslAbilityFactory,
public helperService: HelperService
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers =
this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler()
) || [];

const { user, body } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);
const pHandlers = policyHandlers.every((handler) =>
this.execPolicyHandler(handler, ability, body)
);
if (pHandlers) {
return pHandlers;
} else {
throw new ForbiddenException(
this.helperService.formatReqMessagesString("user.userUnAUth", [])
);
}
}

public execPolicyHandler(
handler: PolicyHandler,
ability: AppAbility,
body: any
) {
if (typeof handler === "function") {
return handler(ability, body);
}
return handler.handle(ability, body);
}
}

export const PoliciesGuardEx = (
injectQuery: boolean,
action?: Action,
subject?: typeof EntitySubject,
onlyInject?: boolean,
dropArrayFields?: boolean
) => {
@Injectable()
class PoliciesGuardMixin implements CanActivate {
constructor(
public reflector: Reflector,
public caslAbilityFactory: CaslAbilityFactory,
public helperService: HelperService
) {}

parseMongoQueryToSQL(mongoQuery, isNot = false, key = undefined) {
let final = undefined;
for (let operator in mongoQuery) {
if (operator.startsWith("$")) {
if (operator == "$and" || operator == "$or") {
const val = mongoQuery[operator].map(st => this.parseMongoQueryToSQL(st)).join(` ${operator.replace("$", '')} `)
final = final == undefined ? val : `${final} and ${val}`
} else if (operator == "$not") {
return this.parseMongoQueryToSQL(mongoQuery["$not"], !isNot)
} else if (operator == "$eq") {
const value = (typeof mongoQuery["$eq"] === "number") ? String(mongoQuery["$eq"]) : `'${mongoQuery["$eq"]}'`
return `"${key}" ${isNot ? "!=" : "="} ${value}`
} else if (operator == "$ne") {
const value = (typeof mongoQuery["$ne"] === "number") ? String(mongoQuery["$ne"]) : `'${mongoQuery["$ne"]}'`
return `"${key}" ${isNot ? "=" : "!="} ${value}`
}
} else {
return this.parseMongoQueryToSQL(mongoQuery[operator], isNot, operator)
}
}
return final;
}

async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers =
this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler()
) || [];

const { user, body } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);

if (injectQuery) {
context.switchToHttp().getRequest()['ability'] = ability;

const mongoQuery = JSON.stringify(rulesToQuery(ability, action, subject, rule => {
return rule.inverted ? { $not: rule.conditions } : rule.conditions
}))
console.log(JSON.stringify(mongoQuery))

if (mongoQuery && mongoQuery != "" && mongoQuery != "{}" && mongoQuery != '{"$or":[{}]}') {
// const whereQuery = this.parseMongoQueryToSQL(JSON.parse(mongoQuery));
// console.log("Where", whereQuery)
context.switchToHttp().getRequest()['abilityCondition'] = JSON.parse(mongoQuery);
}
}

if (dropArrayFields) {
const obj = Object.assign(new subject(), body);
let abilityCan: boolean = true;
for (const key in obj) {
const possible = [];
if (obj[key] instanceof Array) {
// console.log(obj[key]);
for (const en of obj[key]) {
for (const key2 in en) {
// console.log(action, en, key2);
if (ability.can(action, plainToClass(Stat, en), key2)) {
possible.push(en);
}
}
}
obj[key] = possible;
context.switchToHttp().getRequest()["body"] = obj;
abilityCan = possible.length > 0;
if (abilityCan) {
return abilityCan;
} else {
throw new ForbiddenException(
this.helperService.formatReqMessagesString(
"user.userUnAUth",
[]
)
);
}
}
}
}

if (policyHandlers.length == 0 && action && subject && !onlyInject) {
const obj = Object.assign(new subject(), body);
let abilityCan: boolean = true;
// console.log(obj);
if (action == Action.Update) {
// if (obj instanceof User && obj.organisationId == undefined) {
// obj.organisationId = user.companyId;
// }
for (const key in obj) {
if (!ability.can(action, obj, key)) {
console.log(
"Failed due to",
JSON.stringify(ability),
action,
obj,
key
);
abilityCan = false;
}
}
} else if (action == Action.Delete) {
abilityCan = ability.can(action, subject);
} else {
abilityCan = ability.can(action, obj);
}
if (abilityCan) {
return abilityCan;
} else {
throw new ForbiddenException(
this.helperService.formatReqMessagesString("user.userUnAUth", [])
);
}
}

const pHandler = policyHandlers.every((handler) =>
this.execPolicyHandler(handler, ability, body)
);
if (pHandler) {
return pHandler;
} else {
throw new ForbiddenException(
this.helperService.formatReqMessagesString("user.userUnAUth", [])
);
}
}

public execPolicyHandler(
handler: PolicyHandler,
ability: AppAbility,
body: any
) {
if (typeof handler === "function") {
return handler(ability, body);
}
return handler.handle(ability, body);
}
}

const guard = mixin(PoliciesGuardMixin);
return guard;
};
9 changes: 9 additions & 0 deletions backend/services/src/casl/policy.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AppAbility } from './casl-ability.factory';

interface IPolicyHandler {
handle(ability: AppAbility, req: any): boolean;
}

type PolicyHandlerCallback = (ability: AppAbility, req: any) => boolean;

export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
19 changes: 19 additions & 0 deletions backend/services/src/casl/role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export enum Role {
Observer = 'Observer',
GovernmentUser = 'GovernmentUser',
Admin = 'Admin',
Root = 'Root',
}

export enum SubRole {
GovernmentDepartment = 'GovernmentDepartment',
Consultant = 'Consultant',
SEO = 'SEO',
TechnicalReviewer = 'TechnicalReviewer',
DevelopmentPartner = 'DevelopmentPartner'
}

export const roleSubRoleMap = {
'GovernmentUser': ['GovernmentDepartment', 'Consultant', 'SEO'],
'Observer': ['TechnicalReviewer', 'DevelopmentPartner']
}
18 changes: 18 additions & 0 deletions backend/services/src/casl/sectoralSecor.mapped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const sectoralScopesMapped: any = {
Energy: [
'Energy Industries (Renewable – / Non-Renewable Sources)',
'Energy Distribution',
'Energy Demand',
],
Transport: ['Transport'],
Manufacturing: ['Manufacturing Industries', 'Chemical Industries', 'Metal Production'],
Forestry: ['Afforestation and Reforestation'],
Waste: ['Waste Handling and Disposal', 'Fugitive Emissions From Fuels (Solid, Oil and Gas)'],
Agriculture: ['Agriculture'],
Other: [
'Mining/Mineral Production',
'Construction',
'Fugitive Emissions From Production and Consumption of Halocarbons and Sulphur Hexafluoride',
'Solvent Use',
],
};
94 changes: 94 additions & 0 deletions backend/services/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export default () => ({
stage: process.env.STAGE || "local",
systemCountry: process.env.systemCountryCode || "NG",
systemCountryName: process.env.systemCountryName || "CountryX",
systemCountryGovernmentName: process.env.systemCountryGovernmentName || "Government of CountryX",
systemContinentName: process.env.systemContinentName || "CountryX",
defaultCreditUnit: process.env.defaultCreditUnit || "ITMO",
year: parseInt(process.env.REPORT_YEAR),
dateTimeFormat: "DD LLLL yyyy @ HH:mm",
dateFormat: "DD LLLL yyyy",
database: {
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USER || "hquser",
password: process.env.DB_PASSWORD || "",
database: process.env.DB_NAME || "carbondev",
synchronize: process.env.NODE_ENV == "prod" ? true : true,
autoLoadEntities: true,
logging: ["error"],
},
jwt: {
expiresIn: process.env.EXPIRES_IN || "7200",
userSecret: process.env.USER_JWT_SECRET || "1324",
adminSecret: process.env.ADMIN_JWT_SECRET || "8654",
encodePassword: process.env.ENCODE_PASSWORD || false
},
ledger: {
name: "carbon-registry-" + (process.env.NODE_ENV || "dev"),
table: "programmes",
overallTable: "overall",
companyTable: "company",
},
email: {
source: process.env.SOURCE_EMAIL || "info@xeptagon.com",
endpoint:
process.env.SMTP_ENDPOINT ||
"vpce-02cef9e74f152b675-b00ybiai.email-smtp.us-east-1.vpce.amazonaws.com",
username: process.env.SMTP_USERNAME || "AKIAUMXKTXDJIOFY2QXL",
password: process.env.SMTP_PASSWORD,
disabled: process.env.IS_EMAIL_DISABLED === "true" ? true : false,
disableLowPriorityEmails:
process.env.DISABLE_LOW_PRIORITY_EMAIL === "true" ? true : false,
// getemailprefix: process.env.EMAILPREFIX || "🏬📐 🇦🇶",
getemailprefix: process.env.EMAILPREFIX || "",
adresss: process.env.HOST_ADDRESS || "Address <br>Region, Country Zipcode"
},
s3CommonBucket: {
name: process.env.S3_COMMON_BUCKET || "carbon-common-dev",
},
host: process.env.HOST || "https://test.carbreg.org",
backendHost: process.env.BACKEND_HOST || "http://localhost:3000",
liveChat: "https://undp2020cdo.typeform.com/to/emSWOmDo",
mapbox: {
key: process.env.MAPBOX_PK,
},
openstreet: {
retrieve: process.env.OPENSTREET_QUERY === "true" || false,
},
asyncQueueName:
process.env.ASYNC_QUEUE_NAME ||
"https://sqs.us-east-1.amazonaws.com/302213478610/AsyncQueuedev.fifo",
ITMOSystem: {
endpoint:
process.env.ITMO_ENDPOINT ||
"https://dev-digital-carbon-finance-webapp-api-rxloyxnj3dbso.azurewebsites.net/api/v1/",
apiKey: process.env.ITMO_API_KEY,
email: process.env.ITMO_EMAIL,
password: process.env.ITMO_PASSWORD,
enable: process.env.ITMO_ENABLE === "true" ? true : false,
},
CERTIFIER:{
image:process.env.CERTIFIER_IMAGE
},
registry: {
syncEnable: process.env.SYNC_ENABLE === "true" ? true : false,
endpoint: process.env.SYNC_ENDPOINT || 'https://u4h9swxm8b.execute-api.us-east-1.amazonaws.com/dev',
apiToken: process.env.SYNC_API_TOKEN
},
docGenerate: {
ministerName: process.env.MINISTER_NAME || 'Minister X',
ministerNameAndDesignation: process.env.MINISTER_NAME_AND_DESIGNATION || '\nHonorable Minister X\nMinister\nMinistry of Environment, Forestry & Tourism',
ministryName: "Ministry of Environment, Forestry & Tourism",
countryCapital: process.env.COUNTRY_CAPITAL || "Capital X",
contactEmailForQuestions: process.env.CONTACT_EMAIL || "contactus@email.com"
},
cadTrust: {
enable: process.env.CADTRUST_ENABLE === "true" ? true : false,
endpoint: process.env.CADTRUST_ENDPOINT || "http://44.212.139.61:31310/"
},
systemType: process.env.SYSTEM_TYPE || "CARBON_UNIFIED_SYSTEM",
systemName: process.env.SYSTEM_NAME || "SystemX",
environmentalManagementActHyperlink: process.env.ENVIRONMENTAL_MANAGEMENT_ACT_HYPERLINK || "",
});
1 change: 1 addition & 0 deletions backend/services/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const API_KEY_SEPARATOR = '___'
24 changes: 12 additions & 12 deletions backend/services/src/data-importer/handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// lambda.ts
import { Handler, Context } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { getLogger } from '@undp/carbon-services-lib';
import { DataImporterModule } from '@undp/carbon-services-lib';
import { DataImporterService } from '@undp/carbon-services-lib';
// // lambda.ts
// import { Handler, Context } from 'aws-lambda';
// import { NestFactory } from '@nestjs/core';
// import { getLogger } from '@undp/carbon-services-lib';
// import { DataImporterModule } from '@undp/carbon-services-lib';
// import { DataImporterService } from '@undp/carbon-services-lib';

export const handler: Handler = async (event: any, context: Context) => {
const app = await NestFactory.createApplicationContext(DataImporterModule, {
logger: getLogger(DataImporterModule),
});
await app.get(DataImporterService).importData(event);
}
// export const handler: Handler = async (event: any, context: Context) => {
// const app = await NestFactory.createApplicationContext(DataImporterModule, {
// logger: getLogger(DataImporterModule),
// });
// await app.get(DataImporterService).importData(event);
// }
40 changes: 40 additions & 0 deletions backend/services/src/dtos/achievementDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { ArrayMinSize, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { IsTwoDecimalPoints } from "../util/twoDecimalPointNumber.decorator";

export class AchievementDto {

@IsNotEmpty()
@IsNumber()
@ApiProperty()
kpiId: number;

@IsNotEmpty()
@IsString()
@ApiProperty()
activityId: string;

@IsTwoDecimalPoints()
@IsNotEmpty()
@IsNumber()
@ApiProperty()
achieved: number;
}

export class AchievementDtoList {

@ArrayMinSize(1)
@IsNotEmpty({ each: true })
@ApiProperty({
type: "array",
example: [{
kpiId: "1",
activityId: "T00001",
achieved: 100
}],
items: {
$ref: getSchemaPath(AchievementDto),
},
})
achievements: AchievementDto[]
}
131 changes: 131 additions & 0 deletions backend/services/src/dtos/action.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsString, IsOptional, ValidateNested, IsNumber, Min, Max, MaxLength, ArrayMinSize, IsArray } from "class-validator";
import { ActionStatus, InstrumentType, NatAnchor } from "../enums/action.enum";
import { KpiDto } from "./kpi.dto";
import { DocumentDto } from "./document.dto";
import { KpiUnits } from "../enums/kpi.enum";
import { Sector } from "../enums/sector.enum";
import { ActionType } from "../enums/action.enum";

export class ActionDto {

actionId: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
title: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
description: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
objective: string;

@IsNotEmpty()
@IsEnum(Sector, {
each: true,
message: 'Invalid Affected Sector. Supported following types:' + Object.values(Sector)
})
@ApiProperty({
type: [String],
enum: Object.values(Sector),
})
sector: Sector;

@IsNotEmpty()
@IsEnum(ActionType, {
each: true,
message: 'Invalid Action Type:' + Object.values(ActionType)
})
@ApiProperty({
type: [String],
enum: Object.values(ActionType),
})
type: ActionType;

@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(InstrumentType, {
each: true,
message: "Invalid instrument type. Supported following instrument types:" + Object.values(InstrumentType),
})
@ApiProperty({
type: [String],
enum: Object.values(InstrumentType),
})
instrumentType: InstrumentType[];

@IsNotEmpty()
@ApiProperty({ enum: ActionStatus })
@IsEnum(ActionStatus, {
message: "Invalid status. Supported following statuses:" + Object.values(ActionStatus),
})
status: ActionStatus;

@IsNotEmpty()
@IsNumber()
@Min(2013)
@Max(2049)
@ApiProperty()
startYear: number;

@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(NatAnchor, {
each: true,
message: "Invalid Anchored National Strategy. Supported following strategies:" + Object.values(NatAnchor),
})
@ApiProperty({
type: [String],
enum: Object.values(NatAnchor),
})
natAnchor: NatAnchor[];

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: [{
title: "document 1",
data: "base64 document string"
}],
items: {
$ref: getSchemaPath(DocumentDto),
},
}
)
documents: DocumentDto[];

@IsOptional()
@ApiPropertyOptional()
linkedProgrammes: string[];

@IsOptional()
@ValidateNested()
@ApiPropertyOptional(
{
type: "array",
example: [{
name: "KPI 1",
kpiUnit: KpiUnits.GWp_INSTALLED,
creatorType: "action",
expected: 100
}],
items: {
$ref: getSchemaPath(KpiDto),
},
}
)
kpis: KpiDto[];


}
140 changes: 140 additions & 0 deletions backend/services/src/dtos/actionUpdate.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsString, IsOptional, ValidateNested, IsNumber, Min, Max, isNotEmpty, ArrayMinSize, MaxLength, IsArray } from "class-validator";
import { ActionStatus, ActionType, InstrumentType, NatAnchor } from "../enums/action.enum";
import { KpiDto } from "./kpi.dto";
import { DocumentDto } from "./document.dto";
import { KpiUpdateDto } from "./kpi.update.dto";
import { KpiUnits } from "../enums/kpi.enum";
import { Sector } from "../enums/sector.enum";
import { KPIAction } from "../enums/shared.enum";

export class ActionUpdateDto {

@IsString()
@IsNotEmpty()
@ApiProperty()
actionId: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
title: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
description: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
objective: string;

@IsNotEmpty()
@IsEnum(Sector, {
each: true,
message: 'Invalid Affected Sector. Supported following types:' + Object.values(Sector)
})
@ApiProperty({
type: [String],
enum: Object.values(Sector),
})
sector: Sector;

@IsNotEmpty()
@IsEnum(ActionType, {
each: true,
message: 'Invalid Action Type:' + Object.values(ActionType)
})
@ApiProperty({
type: [String],
enum: Object.values(ActionType),
})
type: ActionType;

@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(InstrumentType, {
each: true,
message: "Invalid instrument type. Supported following instrument types:" + Object.values(InstrumentType),
})
@ApiProperty({
type: [String],
enum: Object.values(InstrumentType),
})
instrumentType: InstrumentType[];

@IsNotEmpty()
@ApiProperty({ enum: ActionStatus })
@IsEnum(ActionStatus, {
message: "Invalid status. Supported following statuses:" + Object.values(ActionStatus),
})
status: ActionStatus;

@IsNotEmpty()
@IsNumber()
@Min(2013)
@Max(2049)
@ApiProperty()
startYear: number;

@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(NatAnchor, {
each: true,
message: "Invalid Anchored National Strategy. Supported following strategies:" + Object.values(NatAnchor),
})
@ApiProperty({
type: [String],
enum: Object.values(NatAnchor),
})
natAnchor: NatAnchor[];

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: [{
title: "document 1",
data: "base64 document string"
}],
items: {
$ref: getSchemaPath(DocumentDto),
},
}
)
newDocuments: DocumentDto[];

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: ["http://test.com/documents/action_documents/testDoc1_1713334127897.csv"],
}
)
removedDocuments: string[];

@IsOptional()
@ValidateNested()
@ApiPropertyOptional(
{
type: "array",
example: [{
kpiId: "1",
kpiUnit: KpiUnits.GWp_INSTALLED,
name: "KPI 1",
creatorType: "action",
expected: 100,
KPIAction: KPIAction.CREATED, // To check KPI is Updated or not for Update Timeline
}],
items: {
$ref: getSchemaPath(KpiUpdateDto),
},
}
)
kpis: KpiUpdateDto[];

}
202 changes: 202 additions & 0 deletions backend/services/src/dtos/activity.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength, ValidateIf } from "class-validator";
import { ActivityStatus, ImpleMeans, Measure, SupportType, TechnologyType } from "../enums/activity.enum";
import { EntityType, GHGS, IntImplementor, NatImplementor } from "../enums/shared.enum";
import { DocumentDto } from "./document.dto";
import { Type } from "class-transformer";

export class ActivityDto {

activityId: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
title: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
description: string;

@IsNotEmpty()
@ApiProperty({ enum: [EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT] })
@IsIn([EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT], {
message: 'Invalid Entity Type. Supported types are:' + Object.values([EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT]),
})
parentType: EntityType;

@IsNotEmpty()
@IsString()
@ApiProperty()
parentId: string;

@ValidateIf((c) => c.measure)
@IsNotEmpty()
@ApiPropertyOptional({ enum: Measure })
@IsEnum(Measure, {
message: "Invalid Measure type. Supported following types:" + Object.values(Measure),
})
measure: Measure;

@IsNotEmpty()
@ApiProperty({ enum: ActivityStatus })
@IsEnum(ActivityStatus, {
message: "Invalid activity status. Supported following status:" + Object.values(ActivityStatus),
})
status: ActivityStatus;

@ValidateIf((c) => c.nationalImplementingEntity)
@IsOptional()
@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(NatImplementor, {
each: true,
message: 'Invalid National Implementing Entity. Supported following entities:' + Object.values(NatImplementor)
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(NatImplementor),
})
nationalImplementingEntity: NatImplementor[]

@ValidateIf((c) => c.internationalImplementingEntity)
@IsOptional()
@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(IntImplementor, {
each: true,
message: 'Invalid International Implementing Entity. Supported following entities:' + Object.values(IntImplementor)
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(IntImplementor),
})
internationalImplementingEntity: IntImplementor[]

@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
anchoredInNationalStrategy: boolean;

@ValidateIf((c) => c.meansOfImplementation)
@IsNotEmpty()
@ApiPropertyOptional({ enum: ImpleMeans })
@IsEnum(ImpleMeans, {
message: "Invalid Means of Implementation. Supported following types:" + Object.values(ImpleMeans),
})
meansOfImplementation: ImpleMeans;

@ValidateIf((c) => c.technologyType)
@IsNotEmpty()
@ApiPropertyOptional({ enum: TechnologyType })
@IsEnum(TechnologyType, {
message: "Invalid Technology Type. Supported following types:" + Object.values(TechnologyType),
})
technologyType: TechnologyType;

@IsOptional()
@IsString()
@ApiPropertyOptional()
etfDescription: string;

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: [{
title: "document 1",
data: "base64 document string"
}],
items: {
$ref: getSchemaPath(DocumentDto),
},
}
)
documents: DocumentDto[];

@ValidateIf((c) => c.ghgsAffected)
@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(GHGS, {
each: true,
message: "Invalid GHG. Supported following types:" + Object.values(GHGS),
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(GHGS),
})
ghgsAffected: GHGS[];

@IsNumber()
@ApiProperty()
achievedGHGReduction: number;

@IsNumber()
@ApiProperty()
expectedGHGReduction: number;

@IsOptional()
@ApiPropertyOptional()
@IsString()
comments: string;

@IsOptional()
@ApiPropertyOptional({
type: "object",
example: {
mitigationMethodology: "CO2",
mitigationMethodologyDescription: "test",
mitigationCalcEntity: "ABB",
comments: "test mitigation comments",
methodologyDocuments: [{
title: "mitigation document 1",
data: "base64 document string"
}],
resultDocuments: [{
title: "result document 1",
data: "base64 document string"
}]
},
})
mitigationInfo: any;

@IsOptional()
@ApiPropertyOptional({
type: "object",
example: {
expected: {
baselineEmissions: [7,8,9,7],
activityEmissionsWithM: [7,8,9,7],
activityEmissionsWithAM: [7,8,9,7],
expectedEmissionReductWithM: [7,8,9,7],
expectedEmissionReductWithAM: [7,8,9,7],
total: {
baselineEmissions:31,
activityEmissionsWithM:31,
activityEmissionsWithAM:31,
expectedEmissionReductWithM:31,
expectedEmissionReductWithAM:31,
}
},
actual: {
baselineActualEmissions: [7,8,9,7],
activityActualEmissions: [7,8,9,7],
actualEmissionReduct: [7,8,9,7],
total: {
baselineActualEmissions:31,
activityActualEmissions:31,
actualEmissionReduct:31,
}
},
startYear: 2015,
},
})
mitigationTimeline: any;
}
47 changes: 47 additions & 0 deletions backend/services/src/dtos/activity.response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ActivityStatus, ImpleMeans, Measure, TechnologyType } from "../enums/activity.enum";
import { EntityType, IntImplementor, NatImplementor } from "../enums/shared.enum";
import { DocumentDto } from "./document.dto";

export class ActivityResponseDto {

activityId: string;

title: string;

description: string;

parentType: EntityType;

parentId: string;

measure: Measure;

status: ActivityStatus;

nationalImplementingEntity: NatImplementor[]

internationalImplementingEntity: IntImplementor[]

anchoredInNationalStrategy: boolean;

meansOfImplementation: ImpleMeans;

technologyType: TechnologyType;

etfDescription: string;

documents: DocumentDto[];

achievedGHGReduction: number;

expectedGHGReduction: number;

comments: string;

mitigationInfo: any;

mitigationTimeline: any;

migratedData: any;

}
196 changes: 196 additions & 0 deletions backend/services/src/dtos/activityUpdate.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength, ValidateIf } from "class-validator";
import { ActivityStatus, ImpleMeans, Measure, TechnologyType } from "../enums/activity.enum";
import { EntityType, GHGS, IntImplementor, NatImplementor } from "../enums/shared.enum";
import { DocumentDto } from "./document.dto";

export class ActivityUpdateDto {

@IsNotEmpty()
@IsString()
@ApiProperty()
activityId: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
title: string;

@IsNotEmpty()
@IsString()
@ApiProperty()
description: string;

@ValidateIf((c) => c.parentType)
@IsNotEmpty()
@ApiProperty({ enum: [EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT] })
@IsIn([EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT], {
message: 'Invalid Entity Type. Supported types are:' + Object.values([EntityType.ACTION, EntityType.PROGRAMME, EntityType.PROJECT]),
})
parentType: EntityType;

@IsOptional()
@IsString()
@ApiPropertyOptional()
parentId: string;

@ValidateIf((c) => c.measure)
@IsNotEmpty()
@ApiPropertyOptional({ enum: Measure })
@IsEnum(Measure, {
message: "Invalid Measure type. Supported following types:" + Object.values(Measure),
})
measure: Measure;

@IsNotEmpty()
@ApiProperty({ enum: ActivityStatus })
@IsEnum(ActivityStatus, {
message: "Invalid activity status. Supported following status:" + Object.values(ActivityStatus),
})
status: ActivityStatus;

@ValidateIf((c) => c.nationalImplementingEntity)
@IsOptional()
@IsArray()
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(NatImplementor, {
each: true,
message: 'Invalid National Implementing Entity. Supported following entities:' + Object.values(NatImplementor)
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(NatImplementor),
})
nationalImplementingEntity: NatImplementor[]

@ValidateIf((c) => c.internationalImplementingEntity)
@IsOptional()
@IsArray()
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(IntImplementor, {
each: true,
message: 'Invalid International Implementing Entity. Supported following entities:' + Object.values(IntImplementor)
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(IntImplementor),
})
internationalImplementingEntity: IntImplementor[]

@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
anchoredInNationalStrategy: boolean;

@ValidateIf((c) => c.meansOfImplementation)
@IsNotEmpty()
@ApiPropertyOptional({ enum: ImpleMeans })
@IsEnum(ImpleMeans, {
message: "Invalid Means of Implementation. Supported following types:" + Object.values(ImpleMeans),
})
meansOfImplementation: ImpleMeans;

@ValidateIf((c) => c.technologyType)
@IsNotEmpty()
@ApiPropertyOptional({ enum: TechnologyType })
@IsEnum(TechnologyType, {
message: "Invalid Technology Type. Supported following types:" + Object.values(TechnologyType),
})
technologyType: TechnologyType;

@IsOptional()
@IsString()
@ApiPropertyOptional()
etfDescription: string;

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: [{
title: "document 1",
data: "base64 document string"
}],
items: {
$ref: getSchemaPath(DocumentDto),
},
}
)
newDocuments: DocumentDto[];

@IsOptional()
@ApiPropertyOptional(
{
type: "array",
example: ["http://test.com/documents/activity_documents/testDoc1_1713334127897.csv"],
}
)
removedDocuments: string[];

@ValidateIf((c) => c.ghgsAffected)
@IsArray()
@ArrayMinSize(1)
@MaxLength(100, { each: true })
@IsNotEmpty({ each: true })
@IsEnum(GHGS, {
each: true,
message: "Invalid GHG. Supported following types:" + Object.values(GHGS),
})
@ApiPropertyOptional({
type: [String],
enum: Object.values(GHGS),
})
ghgsAffected: GHGS[];

@IsNumber()
@ApiProperty()
achievedGHGReduction: number;

@IsNumber()
@ApiProperty()
expectedGHGReduction: number;

@IsOptional()
@ApiPropertyOptional()
@IsString()
comments: string;

@IsOptional()
@ApiPropertyOptional({
type: "object",
example: {
mitigationMethodology: "CO2",
mitigationMethodologyDescription: "test",
mitigationCalcEntity: "ABB",
comments: "test mitigation comments",
methodologyDocuments: [
{
title: "added mitigation document ",
data: "base64 document string"
},
{
createdTime: 12556988775,
title: "existing document",
updatedTime: undefined,
url: "www.test.com/documents/activity_documents/crr_mit_01.pdf",
}
],
resultDocuments: [
{
title: "added result document ",
data: "base64 document string"
},
{
createdTime: 12556988775,
title: "existing document",
updatedTime: undefined,
url: "www.test.com/documents/activity_documents/crr_rep_01.pdf",
}
]
},
})
mitigationInfo: any;

}
5 changes: 5 additions & 0 deletions backend/services/src/dtos/basic.response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class BasicResponseDto {
constructor(public statusCode: number, public message: string) {

}
}
8 changes: 8 additions & 0 deletions backend/services/src/dtos/chartStats.request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export class chartStatsRequestDto {
type: string;
value?: string;
companyId?: any;
startDate?: number;
endDate?: number;
}
9 changes: 9 additions & 0 deletions backend/services/src/dtos/data.count.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class DataCountResponseDto {
stats: any;
lastUpdate: number;

constructor(stats: any, lastUpdate: number) {
this.stats = stats;
this.lastUpdate = lastUpdate;
}
}
1 change: 1 addition & 0 deletions backend/services/src/dtos/data.export.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class DataExportDto {}
Loading

0 comments on commit d1c6100

Please sign in to comment.