From f393657fe1d2401e8f4cea013a41f6f31cc47f61 Mon Sep 17 00:00:00 2001 From: Vikas Jagtap Date: Sun, 15 May 2022 21:40:41 +1000 Subject: [PATCH 1/2] OpenHarvest-13 prepare backend for production with integrations configurable per organisation --- backend/package.json | 225 +++++++++++++++++ backend/src/auth/IBMiDStrategy.ts | 13 +- backend/src/auth/helpers.ts | 5 +- backend/src/db/entities/coopManager.ts | 33 --- backend/src/db/entities/crop.ts | 22 +- backend/src/db/entities/farm.ts | 34 +++ backend/src/db/entities/farmer.ts | 32 +-- backend/src/db/entities/land.ts | 36 --- backend/src/db/entities/messageLog.ts | 5 +- backend/src/db/entities/organisation.ts | 37 ++- backend/src/db/mongodb.ts | 1 - .../src/integrations/EIS/EIS-api.service.ts | 107 -------- backend/src/integrations/EIS/EIS.types.ts | 33 +-- .../src/integrations/EIS/EISFarmService.ts | 116 +++++++++ backend/src/integrations/EIS/EISService.ts | 54 ++++ backend/src/integrations/eventBus.service.ts | 6 +- .../src/integrations/messagingInterface.ts | 18 +- .../integrations/smsSync/smsSync.service.ts | 22 +- .../src/integrations/twilio/twilio.service.ts | 28 ++- .../weather-company-api.service.ts | 64 ----- .../weatherCompany/WeatherCompanyService.ts | 53 ++++ .../weather-company-api.types.ts | 0 backend/src/main.ts | 8 +- backend/src/routes/coopManager-route.ts | 90 ------- backend/src/routes/crop-route.ts | 6 +- backend/src/routes/dashboard-route.ts | 3 +- backend/src/routes/farmer-route.ts | 89 ++----- backend/src/routes/lot-route.ts | 5 +- backend/src/routes/messaging-route.ts | 4 +- backend/src/routes/organisation-route.ts | 39 ++- backend/src/routes/recommendations-route.ts | 3 +- backend/src/routes/sms-route.ts | 7 +- backend/src/routes/user-route.ts | 52 ++++ backend/src/routes/weather-route.ts | 34 ++- .../{crop.service.ts => CropService.ts} | 13 +- backend/src/services/FarmService.ts | 64 +++++ backend/src/services/FarmerService.ts | 32 +++ backend/src/services/OrganisationService.ts | 63 +++++ backend/src/services/UserService.ts | 54 ++++ backend/src/services/coopManager.service.ts | 55 ----- backend/src/services/land-areas.service.ts | 52 ++-- backend/src/services/organisation.service.ts | 32 --- .../src/services/recommendations.service.ts | 77 +++--- backend/src/sockets/socket.io.ts | 8 +- backend/src/types.d.ts | 4 +- common-types/data-model/Farm.ts | 18 ++ common-types/data-model/Field.ts | 15 ++ common-types/data-model/FieldCrop.ts | 7 + .../data-model/{coopManager.ts => User.ts} | 10 +- common-types/data-model/crop.ts | 5 +- common-types/data-model/farmer.ts | 28 +-- common-types/data-model/land.ts | 36 --- common-types/data-model/organisation.ts | 29 +-- common-types/dto/OrganisationDto.ts | 8 + common-types/dto/UserDto.ts | 14 ++ .../globals.ts | 29 +-- common-types/integrations/EISConfig.ts | 6 + .../integrations/WeatherCompanyConfig.ts | 7 + common-types/package.json | 232 ++++++++++++++++++ common-types/types.d.ts | 85 +++++++ docker-compose.yml | 12 + react-app/src/App.tsx | 173 ++++++------- react-app/src/components/Nav/Nav.tsx | 32 +-- react-app/src/services/auth.tsx | 152 +++++------- react-app/src/setupProxy.js | 20 +- 65 files changed, 1627 insertions(+), 1029 deletions(-) delete mode 100644 backend/src/db/entities/coopManager.ts create mode 100644 backend/src/db/entities/farm.ts delete mode 100644 backend/src/db/entities/land.ts delete mode 100644 backend/src/integrations/EIS/EIS-api.service.ts create mode 100644 backend/src/integrations/EIS/EISFarmService.ts create mode 100644 backend/src/integrations/EIS/EISService.ts delete mode 100644 backend/src/integrations/weather-company-api.service.ts create mode 100644 backend/src/integrations/weatherCompany/WeatherCompanyService.ts rename common-types/Fields.ts => backend/src/integrations/weatherCompany/weather-company-api.types.ts (100%) delete mode 100644 backend/src/routes/coopManager-route.ts create mode 100644 backend/src/routes/user-route.ts rename backend/src/services/{crop.service.ts => CropService.ts} (70%) create mode 100644 backend/src/services/FarmService.ts create mode 100644 backend/src/services/FarmerService.ts create mode 100644 backend/src/services/OrganisationService.ts create mode 100644 backend/src/services/UserService.ts delete mode 100644 backend/src/services/coopManager.service.ts delete mode 100644 backend/src/services/organisation.service.ts create mode 100644 common-types/data-model/Farm.ts create mode 100644 common-types/data-model/Field.ts create mode 100644 common-types/data-model/FieldCrop.ts rename common-types/data-model/{coopManager.ts => User.ts} (59%) delete mode 100644 common-types/data-model/land.ts create mode 100644 common-types/dto/OrganisationDto.ts create mode 100644 common-types/dto/UserDto.ts rename backend/src/integrations/weather-company-api.types.ts => common-types/globals.ts (88%) create mode 100644 common-types/integrations/EISConfig.ts create mode 100644 common-types/integrations/WeatherCompanyConfig.ts create mode 100644 common-types/package.json create mode 100644 common-types/types.d.ts diff --git a/backend/package.json b/backend/package.json index 2f4abdbda..fe81a1367 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,7 @@ "@types/geojson": "^7946.0.8", "@types/uuid": "^8.3.4", "axios": "^0.26.0", + "common-types": "file:../common-types", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.0.0", @@ -36,5 +37,229 @@ "nodemon": "^2.0.15", "ts-node": "^10.5.0", "typescript": "^4.5.5" + }, + "eslintConfig": { + "overrides": [ + { + "files": [ + "**/*.ts?(x)" + ], + "extends": [ + "plugin:react/recommended" + ], + "plugins": [ + "eslint-plugin-prefer-arrow", + "eslint-plugin-react", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "warn", + "@typescript-eslint/array-type": [ + "warn", + { + "default": "array" + } + ], + "@typescript-eslint/ban-types": [ + "warn", + { + "types": { + "Object": { + "message": "Avoid using the `Object` type. Did you mean `object`?" + }, + "Function": { + "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." + }, + "Boolean": { + "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" + }, + "Number": { + "message": "Avoid using the `Number` type. Did you mean `number`?" + }, + "String": { + "message": "Avoid using the `String` type. Did you mean `string`?" + }, + "Symbol": { + "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" + } + } + } + ], + "@typescript-eslint/consistent-type-assertions": "warn", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-member-accessibility": [ + "off", + { + "accessibility": "explicit" + } + ], + "@typescript-eslint/indent": "warn", + "@typescript-eslint/member-delimiter-style": [ + "warn", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ], + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": [ + "warn", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-shadow": [ + "warn", + { + "hoist": "all" + } + ], + "@typescript-eslint/no-unused-expressions": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/quotes": [ + "warn", + "double" + ], + "@typescript-eslint/semi": [ + "warn", + "always" + ], + "@typescript-eslint/triple-slash-reference": [ + "warn", + { + "path": "always", + "types": "prefer-import", + "lib": "always" + } + ], + "@typescript-eslint/type-annotation-spacing": "warn", + "@typescript-eslint/unified-signatures": "warn", + "brace-style": [ + "warn", + "1tbs" + ], + "complexity": "off", + "constructor-super": "warn", + "curly": "warn", + "eol-last": "warn", + "eqeqeq": [ + "warn", + "smart" + ], + "guard-for-in": "warn", + "id-blacklist": [ + "warn", + "any", + "Number", + "number", + "String", + "string", + "Boolean", + "boolean", + "Undefined", + "undefined" + ], + "id-match": "warn", + "indent": "off", + "max-classes-per-file": [ + "warn", + 1 + ], + "max-len": [ + "warn", + { + "code": 200 + } + ], + "new-parens": "warn", + "no-bitwise": "warn", + "no-caller": "warn", + "no-cond-assign": "warn", + "no-console": [ + "warn", + { + "allow": [ + "log", + "warn", + "dir", + "timeLog", + "assert", + "clear", + "count", + "countReset", + "group", + "groupEnd", + "table", + "dirxml", + "error", + "groupCollapsed", + "Console", + "profile", + "profileEnd", + "timeStamp", + "context" + ] + } + ], + "no-debugger": "warn", + "no-empty": "off", + "no-eval": "warn", + "no-fallthrough": "warn", + "no-invalid-this": "off", + "no-new-wrappers": "warn", + "no-redeclare": "warn", + "no-restricted-imports": "warn", + "no-throw-literal": "warn", + "no-trailing-spaces": "warn", + "no-undef-init": "warn", + "no-underscore-dangle": "off", + "no-unsafe-finally": "warn", + "no-unused-labels": "warn", + "no-var": "warn", + "object-shorthand": "warn", + "one-var": [ + "warn", + "never" + ], + "prefer-arrow/prefer-arrow-functions": "warn", + "prefer-const": "warn", + "radix": "warn", + "react/jsx-boolean-value": "warn", + "react/jsx-key": "warn", + "react/jsx-no-bind": "warn", + "react/self-closing-comp": "warn", + "spaced-comment": [ + "warn", + "always", + { + "markers": [ + "/" + ] + } + ], + "use-isnan": "warn", + "valid-typeof": "off", + "react/display-name": "warn" + } + } + ] } } diff --git a/backend/src/auth/IBMiDStrategy.ts b/backend/src/auth/IBMiDStrategy.ts index e5d192127..793b22ed5 100644 --- a/backend/src/auth/IBMiDStrategy.ts +++ b/backend/src/auth/IBMiDStrategy.ts @@ -1,6 +1,5 @@ import { IDaaSOIDCStrategy as OpenIDConnectStrategy } from "passport-ci-oidc"; -import { getCoopManager } from "./../services/coopManager.service"; -import { getOrganisations } from "./../services/organisation.service"; +import { userService } from "../services/UserService"; export const IBMidStrategy = new OpenIDConnectStrategy({ discoveryURL: process.env.AUTH_discovery_url, @@ -17,19 +16,17 @@ export const IBMidStrategy = new OpenIDConnectStrategy({ profile.refreshToken = refreshToken; // Get the farmer coop details const id = "IBMid:" + profile.id; - const doc = await getCoopManager(id); + const doc = await userService.getUser(id); if (doc) { profile.isOnboarded = true; - profile.coopManager = doc.toObject(); - profile.organisations = await getOrganisations(doc.coopOrganisations); - profile.selectedOrganisation = profile.organisations[0]; + profile.user = doc; } else { profile.isOnboarded = false; - profile.coopManager = null; + profile.user = null; } console.log(profile); done(null, profile); }); } -) \ No newline at end of file +) diff --git a/backend/src/auth/helpers.ts b/backend/src/auth/helpers.ts index 728a0aa7a..4d09e7cd4 100644 --- a/backend/src/auth/helpers.ts +++ b/backend/src/auth/helpers.ts @@ -1,5 +1,4 @@ -import { CoopManager } from "./../db/entities/coopManager"; -import { Organisation } from "./../db/entities/organisation"; +import { Organisation, User } from "common-types"; export interface CoopManagerUser { id: string; @@ -15,7 +14,7 @@ export interface CoopManagerUser { exp: number; accessToken: string; refreshToken: string; - coopManager: CoopManager | null; + coopManager: User | null; organisations: Organisation[]; selectedOrganisation: Organisation; } diff --git a/backend/src/db/entities/coopManager.ts b/backend/src/db/entities/coopManager.ts deleted file mode 100644 index baf91f6fd..000000000 --- a/backend/src/db/entities/coopManager.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface CoopManager { - /** - * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" - */ - _id?: string, - /** - * GeoCode / LatLng coordinate tuple - */ - location: number[], - mobile: string, - coopOrganisations: string[] -} - -export const CoopManagerSchema = new Schema({ - /** - * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" - */ - _id: String, - /** - * GeoCode / LatLng coordinate tuple - */ - location: [Number], - mobile: String, - coopOrganisations: [String] -}); - -export const CoopManagerModel = model("coopManager", CoopManagerSchema); diff --git a/backend/src/db/entities/crop.ts b/backend/src/db/entities/crop.ts index 44cc8b66b..24dc11a04 100644 --- a/backend/src/db/entities/crop.ts +++ b/backend/src/db/entities/crop.ts @@ -1,28 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; -import { Schema, model, ObjectId, Types } from 'mongoose'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Crop { - _id?: Types.ObjectId, - type: string, - name: string, - // Start Day of year to end Day of year when to plant the ground nuts - planting_season: number[], - time_to_harvest: number, - is_ongoing: boolean, - yield_per_sqm: number -} +import { Crop } from "common-types" // Mongoose will automatically add _id property. -export const CropSchema = new Schema({ +export const CropSchema = new Schema({ + _id: Types.ObjectId, type: String, name: String, // Start Day of year to end Day of year when to plant the ground nuts planting_season: [Number], time_to_harvest: Number, - is_ongoing: Boolean, yield_per_sqm: Number }); -export const CropModel = model("crop", CropSchema); \ No newline at end of file +export const CropModel = model("crop", CropSchema); diff --git a/backend/src/db/entities/farm.ts b/backend/src/db/entities/farm.ts new file mode 100644 index 000000000..f64fa7608 --- /dev/null +++ b/backend/src/db/entities/farm.ts @@ -0,0 +1,34 @@ +import { model, Schema, Types } from 'mongoose'; +import { FarmerSchema } from './farmer'; + +import { Field, FieldCrop, NewFarm } from "common-types" +import { CropSchema } from "./crop"; + +export const FieldCropSchema = new Schema({ + crop: CropSchema, + planted_date: Date, + harvested_date: Date +}, {id: false}); + +export const FieldSchema = new Schema({ + _id: Types.ObjectId, + name: String, + crops: [FieldCropSchema], + geoShape: { + type: "Polygon", + coordinates: [[Number]] + }, +}); + +export const FarmSchema = new Schema({ + _id: Types.ObjectId, + farmer: FarmerSchema, + name: String, + fields: [FieldSchema], + geoShape: { + type: "Polygon", + coordinates: [[Number]] + }, +}); + +export const FarmModel = model("farm", FarmSchema); diff --git a/backend/src/db/entities/farmer.ts b/backend/src/db/entities/farmer.ts index e34e97810..c313817a8 100644 --- a/backend/src/db/entities/farmer.ts +++ b/backend/src/db/entities/farmer.ts @@ -1,30 +1,14 @@ +import { model, Schema, Types } from 'mongoose'; +import { Farmer } from "common-types" +import { FarmSchema } from "./farm"; -import { FieldResponse } from './../../integrations/EIS/EIS.types'; -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Farmer { - _id?: Types.ObjectId, - name: string, - mobile: string, - address: string, - coopOrganisations: string[], - fieldCount: number; - field?: FieldResponse; -} - -export const FarmerSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, +export const FarmerSchema = new Schema({ + _id: Types.ObjectId, name: String, mobile: String, address: String, - coopOrganisations: [String], - fieldCount: Number + organisation: String, + farms: [FarmSchema] }); -export const FarmerModel = model("farmer", FarmerSchema); \ No newline at end of file +export const FarmerModel = model("farmer", FarmerSchema); diff --git a/backend/src/db/entities/land.ts b/backend/src/db/entities/land.ts deleted file mode 100644 index b2a1e2474..000000000 --- a/backend/src/db/entities/land.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Using Node.js `require()` -import { Schema, Model, model, Types } from 'mongoose'; -import { CropSchema, Crop } from "./crop"; -import { FarmerSchema, Farmer } from './farmer'; - -const ObjectId = Schema.Types.ObjectId; - -export interface FarmerCrop { - _id?: Types.ObjectId, - farmer: Farmer, - crop: Crop -} - -export const FarmerCropSchema = new Schema({ - _id: ObjectId, - farmer: FarmerSchema, - crop: CropSchema -}); - -export interface Land { - _id?: Types.ObjectId, - type: string, - fid: number, - name: string, - crops: FarmerCrop[] -} - -export const LandSchema = new Schema({ - _id: ObjectId, - type: String, - fid: Number, - name: String, - crops: [FarmerCropSchema] -}); - -export const LandModel = model("land", LandSchema); \ No newline at end of file diff --git a/backend/src/db/entities/messageLog.ts b/backend/src/db/entities/messageLog.ts index a5a633fee..6e6e0ae38 100644 --- a/backend/src/db/entities/messageLog.ts +++ b/backend/src/db/entities/messageLog.ts @@ -1,5 +1,4 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; +import { model, Schema, Types } from 'mongoose'; const ObjectId = Schema.Types.ObjectId; @@ -56,4 +55,4 @@ export const MessageLogSchema = new Schema({ messageRef: String }); -export const MessageLogModel = model("messageLog", MessageLogSchema); \ No newline at end of file +export const MessageLogModel = model("messageLog", MessageLogSchema); diff --git a/backend/src/db/entities/organisation.ts b/backend/src/db/entities/organisation.ts index 0ec005eb2..129ae1d78 100644 --- a/backend/src/db/entities/organisation.ts +++ b/backend/src/db/entities/organisation.ts @@ -1,20 +1,31 @@ +import { model, Schema } from 'mongoose'; -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; +import { Organisation, User } from "common-types"; -const ObjectId = Schema.Types.ObjectId; +export const UserSchema = new Schema({ + /** + * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" + */ + _id: String, + /** + * GeoCode / LatLng coordinate tuple + */ + location: [{ + type: "Point", + coordinates: [Number] + }], + mobile: String, +}); -export interface Organisation { - _id?: Types.ObjectId, - name: string -} -export const OrganisationSchema = new Schema({ - _id: { - type: ObjectId, - auto: true +export const OrganisationSchema = new Schema({ + name: { + type: String, + unique: true, + required: true, + index: true }, - name: String, -}); + users: [UserSchema] +}, { _id : false }); export const OrganisationModel = model("organisation", OrganisationSchema); diff --git a/backend/src/db/mongodb.ts b/backend/src/db/mongodb.ts index b4e313d70..019ec1d89 100644 --- a/backend/src/db/mongodb.ts +++ b/backend/src/db/mongodb.ts @@ -1,6 +1,5 @@ // Using Node.js `require()` import { connect } from 'mongoose'; -import { writeFile } from "fs/promises"; export async function mongoInit() { // console.log(process.env.mongodb_url); diff --git a/backend/src/integrations/EIS/EIS-api.service.ts b/backend/src/integrations/EIS/EIS-api.service.ts deleted file mode 100644 index b8f4bf20a..000000000 --- a/backend/src/integrations/EIS/EIS-api.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import axios from "axios"; -import { EISField, EISFieldCreateResponse, EISSubFieldSearchReturn, FieldResponse, Fields } from "./EIS.types"; - - -export class EISAPIService { - - access_token = ""; - /** - * This is a millisecond based timestamp of when the token is due to expire. - * If we're 10 minutes away from it we renew the token - */ - expiration = 0; - - baseAPI = "https://foundation.agtech.ibm.com/v2/"; - - authAxios = axios.create({}); - - constructor(private apiKey: string) { - - } - - - async ensureToken(): Promise { - // 10 mins earlier we try to renew the token - if (this.expiration == 0 || Date.now() > (this.expiration - 600000)) { - // Token is expired - const res = await axios.post("https://auth-b2b-twc.ibm.com/Auth/GetBearerForClient", { - "apiKey": this.apiKey, - "clientId": "ibm-agro-api" - }); - - this.access_token = res.data.access_token; - this.expiration = Date.now() + (res.data.expires_in * 1000); - - this.authAxios.defaults.headers.common['Authorization'] = `Bearer ${this.access_token}`; - - return true; - } - return false; - } - - async createField(field: EISField): Promise { - await this.ensureToken(); - - const res = await this.authAxios.post(this.baseAPI + "field", field); - return res.data; - } - - async getFields() { - await this.ensureToken(); - const res = await this.authAxios.get(this.baseAPI + "field"); - return res.data - } - - async getField(uuid: string) { - await this.ensureToken(); - const fieldRes = await this.authAxios.get(this.baseAPI + `field/${uuid}`); - - const field = fieldRes.data; - - // We need to convert the open harvest object from a string to JSON because EIS stores it as a string - for (let i = 0; i < field.subFields.features.length; i++) { - const subField = field.subFields.features[i]; - subField.properties.open_harvest = JSON.parse(subField.properties.open_harvest as any); - } - - return fieldRes.data; - } - - async getFarmerField(farmer_id: string): Promise { - await this.ensureToken(); - - const queryBody = { - "uuidsOnly": false, - "inputType": "SPECIFIED_FIELD", - "includeDeleted": true, - "includeAssetGeometry": true, - "properties": { - open_harvest: { - farmer_id - } - } - }; - - const res = await this.authAxios.post(this.baseAPI + "asset/search", queryBody); - const subfields = res.data; - - if (subfields.totalRecords == 0) { - return null; - } - - // Get the parent reference which points to the field uuid - const fieldUuid = subfields.features[0].parentReference; - - try { - return await this.getField(fieldUuid); - } - catch (e) { - console.error(e); - return null; - } - } - - - - -} \ No newline at end of file diff --git a/backend/src/integrations/EIS/EIS.types.ts b/backend/src/integrations/EIS/EIS.types.ts index 6f988246c..2b4099d1b 100644 --- a/backend/src/integrations/EIS/EIS.types.ts +++ b/backend/src/integrations/EIS/EIS.types.ts @@ -1,6 +1,5 @@ -import { Crop } from "../../db/entities/crop"; -import { Feature, FeatureCollection, Geometry, Polygon } from "geojson"; -import { LatLng } from "integrations/weather-company-api.types"; +import { Feature, FeatureCollection, Polygon } from "geojson"; +import { GeoCodeNumber, NewFieldCrop } from "common-types"; /** * There are many redundant fields you'll notice because EIS's data structures @@ -37,7 +36,7 @@ export interface FieldResponseSubfieldFeatureExtras { projection: 4326; } -export type FieldResponseSubfieldFeature = Feature & FieldResponseSubfieldFeatureExtras; +export type FieldResponseSubfieldFeature = Feature & FieldResponseSubfieldFeatureExtras; export interface FieldResponseSubfield { type: "FeatureCollection", @@ -51,16 +50,10 @@ export interface FieldResponse { subFields: FieldResponseSubfield; } -// Field Create Structures -export interface SubFieldCrop { - planted: Date; - harvested: Date | null; - farmer: string; - /** - * Crop Information - */ - crop: Crop; -} +export type OpenHarvestSubFieldProps = { + farmer_id: string; + crops: NewFieldCrop[] +}; export interface EISSubFieldProperties { farm_name: string @@ -68,10 +61,7 @@ export interface EISSubFieldProperties { /** * When getting a field from EIS this is initially a string and we need to parse the object */ - open_harvest: { - farmer_id: string; - crops: SubFieldCrop[] - } + open_harvest: OpenHarvestSubFieldProps } export interface EISSubField { @@ -109,7 +99,7 @@ export interface EISSubFieldSearchReturnFeatureProperties { east: number; west: number; }; - centroid: LatLng; + centroid: GeoCodeNumber; ianaTimeZone: string; deleted: boolean; inputType: string; @@ -118,10 +108,7 @@ export interface EISSubFieldSearchReturnFeatureProperties { farm_name: string; field_id: string; field_name: string; - open_harvest: { - farmer_id: string; - crops: SubFieldCrop[] - } + open_harvest: OpenHarvestSubFieldProps } export interface EISSubFieldSearchReturnFeatureExtras { diff --git a/backend/src/integrations/EIS/EISFarmService.ts b/backend/src/integrations/EIS/EISFarmService.ts new file mode 100644 index 000000000..1b38ffaa5 --- /dev/null +++ b/backend/src/integrations/EIS/EISFarmService.ts @@ -0,0 +1,116 @@ +import { EISConfig, Farm, Farmer, Field, NewFarm, NewField } from "common-types"; +import { EISField, EISFieldCreateResponse, EISSubField, EISSubFieldProperties, EISSubFieldSearchReturn, FieldResponse, OpenHarvestSubFieldProps } from "./EIS.types"; +import { EISService } from "./EISService"; +import { Feature, FeatureCollection, Polygon } from "geojson"; + +export class EISFarmService extends EISService { + + async getFarmerFarms(eisConfig: EISConfig, farmer: Farmer): Promise { + const eisSession = await this.getToken(eisConfig); + + const queryBody = { + "uuidsOnly": false, + "inputType": "SPECIFIED_FIELD", + "includeDeleted": true, + "includeAssetGeometry": true, + "properties": { + open_harvest: { + farmer_id: farmer?._id + } + } + }; + + const res = await eisSession.authAxios.post(eisSession.eisConfig.apiUrl + "asset/search", queryBody); + const subfields = res.data; + + if (subfields.totalRecords == 0) { + return []; + } + + // Get the parent reference which points to the field uuid + const parentRefs: {[name: string] : string} = {}; + + subfields.features.forEach((subField) => { + parentRefs[subField.parentReference] = subField.parentReference; + }); + + const farms: Farm[] = []; + + try { + for (const parentRef of Object.keys(parentRefs)) { + farms.push(await this.getFarm(eisConfig, parentRef, farmer)); + } + } catch (e) { + console.error(e); + } + return farms; + } + + async saveFarm(eisConfig: EISConfig, farm: NewFarm): Promise { + const eisSession = await this.getToken(eisConfig); + + const eisField: EISField = { + name: farm.name, + subFields: farm.fields.map((field) => EISFarmService.convertToEisSubField(farm.farmer, field)) + }; + + const res = await eisSession.authAxios.post(eisSession.eisConfig.apiUrl + "field", eisField); + + return this.getFarm(eisConfig, res.data.field, farm.farmer); + } + + private async getFarm(eisConfig: EISConfig, uuid: string, farmer: Farmer): Promise { + const eisSession = await this.getToken(eisConfig); + + const fieldRes = await eisSession.authAxios.get(eisSession.eisConfig.apiUrl + `field/${uuid}`); + + const fieldResponse: FieldResponse = fieldRes.data; + + const fields: Field[] = []; + for (const subField of fieldResponse.subFields.features) { + // We need to convert the open harvest object from a string to JSON because EIS stores it as a string + const openHarvestProps: OpenHarvestSubFieldProps = JSON.parse(subField.properties.open_harvest as any); + + let field: Field = { + _id: subField.uuid, + geoShape: subField.geometry, + name: subField.properties.field_name, + crops: openHarvestProps.crops + }; + + fields.push(field); + } + + return {_id: uuid, fields, farmer, name: fieldResponse.properties.name}; + } + + private static convertToEisSubField(farmer: Farmer, field: NewField): EISSubField { + const feature: Feature = { + geometry: field.geoShape, + properties: { + farm_name: field.name, + open_harvest_farmer_id: farmer._id, + open_harvest: { + farmer_id: farmer._id, + crops: field.crops + } + }, + type: "Feature" + } + + const featureCollection: FeatureCollection = { + features: [feature], + type: "FeatureCollection" + } + + return { + geo: { + geojson: featureCollection, + type: "geojson" + }, + name: field.name + } + } +} + +export const eisFarmService: EISFarmService = new EISFarmService(); diff --git a/backend/src/integrations/EIS/EISService.ts b/backend/src/integrations/EIS/EISService.ts new file mode 100644 index 000000000..dcc1eaef3 --- /dev/null +++ b/backend/src/integrations/EIS/EISService.ts @@ -0,0 +1,54 @@ +import axios, { AxiosInstance } from "axios"; +import { EISConfig } from "../../../../common-types/integrations/EISConfig"; +import { isUndefined } from "common-types"; + +type EISSession = { + token: string, + expiration: number, + authAxios: AxiosInstance, + eisConfig: EISConfig +}; + +export abstract class EISService { + private readonly accessTokens: { + [name: string]: EISSession + } = {}; + + /** + * This is a millisecond based timestamp of when the token is due to expire. + * If we're 10 minutes away from it, we renew the token + */ + + // baseAPI = "https://foundation.agtech.ibm.com/v2/"; + //tokenUrl = "https://auth-b2b-twc.ibm.com/Auth/GetBearerForClient"; + + async getToken(eisConfig: EISConfig): Promise { + + const accessTokenKey = `${eisConfig.clientId}:${eisConfig.apiKey}`; + + const accessToken: EISSession = this.accessTokens[accessTokenKey]; + + // 10 minutes earlier we try to renew the token + if (isUndefined(accessToken) || accessToken.expiration == 0 || Date.now() > (accessToken.expiration - 600000)) { + // Token is expired + const res = await axios.post(eisConfig.tokenUrl, { + "apiKey": eisConfig.apiKey, + "clientId": eisConfig.clientId // "ibm-agro-api" + }); + + const authAxios = accessToken?.authAxios || axios.create({}); + const newToken = res.data.access_token; + authAxios.defaults.headers.common['Authorization'] = `Bearer ${newToken}` + + this.accessTokens[accessTokenKey] = { + token: newToken, + expiration: Date.now() + (res.data.expires_in * 1000), + authAxios, + eisConfig + }; + } + return this.accessTokens[accessTokenKey]; + } + +} + diff --git a/backend/src/integrations/eventBus.service.ts b/backend/src/integrations/eventBus.service.ts index 95856e3e0..3a555cdc0 100644 --- a/backend/src/integrations/eventBus.service.ts +++ b/backend/src/integrations/eventBus.service.ts @@ -5,11 +5,11 @@ * will make the move easy as it will just become an interface */ +import { Organisation } from "common-types"; import { EventEmitter } from "events"; -import { Organisation } from "./../db/entities/organisation"; -import { MessageLog } from "./../db/entities/messageLog"; -import { SocketIOManagerInstance } from "./../sockets/socket.io"; +import { MessageLog } from "../db/entities/messageLog"; +import { SocketIOManagerInstance } from "../sockets/socket.io"; export declare interface EventBus { on(event: 'onMessage', listener: (message: MessageLog) => void): this; diff --git a/backend/src/integrations/messagingInterface.ts b/backend/src/integrations/messagingInterface.ts index ddb44e807..21967e344 100644 --- a/backend/src/integrations/messagingInterface.ts +++ b/backend/src/integrations/messagingInterface.ts @@ -1,9 +1,9 @@ import { EventEmitter } from "events"; -import { Farmer, FarmerModel } from "./../db/entities/farmer"; -import { MessageLog } from "./../db/entities/messageLog"; -import { CoopManager } from "./../db/entities/coopManager"; -import { EventBusInstance } from "./../integrations/eventBus.service"; -import { OrganisationModel } from "./../db/entities/organisation"; +import { FarmerModel } from "../db/entities/farmer"; +import { MessageLog } from "../db/entities/messageLog"; +import { EventBusInstance } from "./eventBus.service"; +import { OrganisationModel } from "../db/entities/organisation"; +import { Farmer, User } from "common-types"; export declare interface MessagingInterface { on(event: 'onMessage', listener: (message: MessageLog) => void): this; @@ -30,7 +30,7 @@ export abstract class MessagingInterface extends EventEmitt * @param coopManager CoopManager we're sending a message to. * @param message The string message we want to send. */ - abstract sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise; + abstract sendMessageToCoopManager(coopManager: User, message: string): Promise; /** * Send a message to an arbitrary destination. This is a way of giving flexibility @@ -67,15 +67,13 @@ export abstract class MessagingInterface extends EventEmitt throw new Error("Farmer from MessageLog not Found!"); } - for (const org_id of farmer.coopOrganisations) { - OrganisationModel.findById(org_id).then(org => { + OrganisationModel.findById(farmer.organisation).then(org => { if (org === null) { throw new Error("Org in Farmer not found!"); } EventBusInstance.publishMessage(org, message); }) - } } -} \ No newline at end of file +} diff --git a/backend/src/integrations/smsSync/smsSync.service.ts b/backend/src/integrations/smsSync/smsSync.service.ts index f2e516ecb..9a0728d83 100644 --- a/backend/src/integrations/smsSync/smsSync.service.ts +++ b/backend/src/integrations/smsSync/smsSync.service.ts @@ -1,9 +1,9 @@ -import { CoopManager } from "../../db/entities/coopManager"; -import { Farmer, FarmerModel } from "../../db/entities/farmer"; +import { FarmerModel } from "../../db/entities/farmer"; import { MessagingInterface } from "../messagingInterface"; import { v4 as uuidv4 } from "uuid"; -import { MessageLog, MessageLogModel, Source, Status } from "./../../db/entities/messageLog"; +import { MessageLog, MessageLogModel, Source, Status } from "../../db/entities/messageLog"; +import { Farmer, User } from "common-types"; export interface SMSSyncMessage { to: string; @@ -43,12 +43,16 @@ export class SMSSyncAPI extends MessagingInterface private pendingMessages: SMSSyncMessage[] = []; async sendMessageToFarmer(farmer: Farmer, message: string): Promise { - const number = farmer.mobile; - if (message === undefined || message === null || message === "") { throw new Error("Message is empty!"); } + if (farmer.mobile.length === 0) { + throw new Error("Farmer has no mobile numbers: " + farmer); + } + + const number = farmer.mobile[0]; + const messageRef = await this.sendMessage(number, message); const messageLogEntry: MessageLog = { @@ -61,12 +65,10 @@ export class SMSSyncAPI extends MessagingInterface messageRef: messageRef } - const messageLog = await MessageLogModel.create(messageLogEntry); - - return messageLog; + return await MessageLogModel.create(messageLogEntry); } - async sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise { + async sendMessageToCoopManager(coopManager: User, message: string): Promise { throw new Error("Method not implemented."); // const number = coopManager.mobile; @@ -124,7 +126,7 @@ export class SMSSyncAPI extends MessagingInterface const messageLog = await MessageLogModel.create(messageLogEntry); this.emit("onMessage", messageLog); - this.notify(messageLog) + await this.notify(messageLog) return messageLog; } diff --git a/backend/src/integrations/twilio/twilio.service.ts b/backend/src/integrations/twilio/twilio.service.ts index d835a0556..b16275fc4 100644 --- a/backend/src/integrations/twilio/twilio.service.ts +++ b/backend/src/integrations/twilio/twilio.service.ts @@ -1,8 +1,8 @@ -import twilio, { Twilio} from "twilio"; -import { CoopManager } from "./../../db/entities/coopManager"; -import { Farmer, FarmerModel } from "./../../db/entities/farmer"; -import { MessageLog, MessageLogModel, Source, Status } from "./../../db/entities/messageLog"; -import { MessagingInterface } from "./../../integrations/messagingInterface"; +import { Farmer, User } from "common-types"; +import twilio, { Twilio } from "twilio"; +import { FarmerModel } from "../../db/entities/farmer"; +import { MessageLog, MessageLogModel, Source, Status } from "../../db/entities/messageLog"; +import { MessagingInterface } from "../messagingInterface"; /** * The Message we get from Twilio on our webhook @@ -25,25 +25,29 @@ export interface TwilioMessage { * This class handles interfacing with twilio. * It provides one */ -class TwilioAPI extends MessagingInterface { +export class TwilioAPI extends MessagingInterface { client: Twilio; messagingServiceSid: string; + twilioInstance: TwilioAPI; constructor() { super(); const accountSid = process.env.Twilio_accountSid; const authToken = process.env.Twilio_token; this.messagingServiceSid = process.env.Twilio_messaging_service as string; this.client = twilio(accountSid, authToken); + this.twilioInstance = new TwilioAPI(); } async sendMessageToFarmer(farmer: Farmer, message: string): Promise { - const number = farmer.mobile; - if (message === undefined || message === null || message === "") { throw new Error("Message is empty!"); } + if (farmer.mobile.length === 0) { + throw new Error("Farmer has no mobile numbers: " + farmer); + } + const number = farmer.mobile[0]; const messageRef = await this.sendMessage(number, message); const messageLogEntry: MessageLog = { @@ -56,12 +60,10 @@ class TwilioAPI extends MessagingInterface { messageRef } - const messageLog = await MessageLogModel.create(messageLogEntry); - - return messageLog; + return await MessageLogModel.create(messageLogEntry); } - async sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise { + async sendMessageToCoopManager(coopManager: User, message: string): Promise { throw new Error("Method not implemented."); } @@ -112,4 +114,4 @@ class TwilioAPI extends MessagingInterface { } -export const TwilioInstance = new TwilioAPI(); + diff --git a/backend/src/integrations/weather-company-api.service.ts b/backend/src/integrations/weather-company-api.service.ts deleted file mode 100644 index c1c878b88..000000000 --- a/backend/src/integrations/weather-company-api.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import axios from "axios"; -import { GeoCode, Languages, Units, CommonOptions, Formats } from "./weather-company-api.types"; - -const testForecastData = {"calendarDayTemperatureMax":[21,22,24,24,24,24,24,24,25,25,25,25,24,25,25],"calendarDayTemperatureMin":[18,17,17,17,16,17,17,17,17,17,17,17,17,17,17],"dayOfWeek":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],"expirationTimeUtc":[1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951],"moonPhase":["Waning Gibbous","Waning Gibbous","Waning Gibbous","Waning Gibbous","Last Quarter","Waning Crescent","Waning Crescent","Waning Crescent","Waning Crescent","Waning Crescent","New","Waxing Crescent","Waxing Crescent","Waxing Crescent","Waxing Crescent"],"moonPhaseCode":["WNG","WNG","WNG","WNG","LQ","WNC","WNC","WNC","WNC","WNC","N","WXC","WXC","WXC","WXC"],"moonPhaseDay":[18,19,20,21,22,24,25,26,27,28,29,1,2,3,3],"moonriseTimeLocal":["2022-02-20T21:05:50+0200","2022-02-21T21:47:27+0200","2022-02-22T22:31:47+0200","2022-02-23T23:21:30+0200","","2022-02-25T00:15:43+0200","2022-02-26T01:15:50+0200","2022-02-27T02:18:43+0200","2022-02-28T03:23:17+0200","2022-03-01T04:25:19+0200","2022-03-02T05:25:08+0200","2022-03-03T06:20:36+0200","2022-03-04T07:13:59+0200","2022-03-05T08:04:43+0200","2022-03-06T08:54:39+0200"],"moonriseTimeUtc":[1645383950,1645472847,1645561907,1645651290,null,1645740943,1645830950,1645921123,1646011397,1646101519,1646191508,1646281236,1646370839,1646460283,1646549679],"moonsetTimeLocal":["2022-02-20T08:50:23+0200","2022-02-21T09:43:37+0200","2022-02-22T10:38:23+0200","2022-02-23T11:37:00+0200","2022-02-24T12:37:49+0200","2022-02-25T13:41:32+0200","2022-02-26T14:44:20+0200","2022-02-27T15:45:07+0200","2022-02-28T16:40:27+0200","2022-03-01T17:31:14+0200","2022-03-02T18:16:18+0200","2022-03-03T18:58:16+0200","2022-03-04T19:37:52+0200","2022-03-05T20:15:34+0200","2022-03-06T20:53:52+0200"],"moonsetTimeUtc":[1645339823,1645429417,1645519103,1645609020,1645699069,1645789292,1645879460,1645969507,1646059227,1646148674,1646237778,1646326696,1646415472,1646504134,1646592832],"narrative":["Thunderstorms developing in the afternoon. Highs 20 to 22ºC and lows 16 to 18ºC.","Thunderstorms. Highs 21 to 23ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms developing in the afternoon. Highs 23 to 25ºC and lows 15 to 17ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 15 to 17ºC."],"qpf":[1.64,8,3.47,1.64,7.29,5.77,3.96,3.2,1.8,4.84,4,4.2,4.77,5.43,4.05],"qpfSnow":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"sunriseTimeLocal":["2022-02-20T05:48:00+0200","2022-02-21T05:48:16+0200","2022-02-22T05:48:31+0200","2022-02-23T05:48:45+0200","2022-02-24T05:48:59+0200","2022-02-25T05:49:13+0200","2022-02-26T05:49:26+0200","2022-02-27T05:49:38+0200","2022-02-28T05:49:50+0200","2022-03-01T05:50:02+0200","2022-03-02T05:50:13+0200","2022-03-03T05:50:24+0200","2022-03-04T05:50:34+0200","2022-03-05T05:50:44+0200","2022-03-06T05:50:53+0200"],"sunriseTimeUtc":[1645328880,1645415296,1645501711,1645588125,1645674539,1645760953,1645847366,1645933778,1646020190,1646106602,1646193013,1646279424,1646365834,1646452244,1646538653],"sunsetTimeLocal":["2022-02-20T18:16:07+0200","2022-02-21T18:15:38+0200","2022-02-22T18:15:08+0200","2022-02-23T18:14:38+0200","2022-02-24T18:14:06+0200","2022-02-25T18:13:35+0200","2022-02-26T18:13:02+0200","2022-02-27T18:12:29+0200","2022-02-28T18:11:56+0200","2022-03-01T18:11:21+0200","2022-03-02T18:10:47+0200","2022-03-03T18:10:11+0200","2022-03-04T18:09:36+0200","2022-03-05T18:09:00+0200","2022-03-06T18:08:23+0200"],"sunsetTimeUtc":[1645373767,1645460138,1645546508,1645632878,1645719246,1645805615,1645891982,1645978349,1646064716,1646151081,1646237447,1646323811,1646410176,1646496540,1646582903],"temperatureMax":[21,22,24,24,24,24,24,24,25,25,25,25,24,25,25],"temperatureMin":[17,17,17,16,17,17,17,17,17,17,17,17,17,17,16],"validTimeLocal":["2022-02-20T07:00:00+0200","2022-02-21T07:00:00+0200","2022-02-22T07:00:00+0200","2022-02-23T07:00:00+0200","2022-02-24T07:00:00+0200","2022-02-25T07:00:00+0200","2022-02-26T07:00:00+0200","2022-02-27T07:00:00+0200","2022-02-28T07:00:00+0200","2022-03-01T07:00:00+0200","2022-03-02T07:00:00+0200","2022-03-03T07:00:00+0200","2022-03-04T07:00:00+0200","2022-03-05T07:00:00+0200","2022-03-06T07:00:00+0200"],"validTimeUtc":[1645333200,1645419600,1645506000,1645592400,1645678800,1645765200,1645851600,1645938000,1646024400,1646110800,1646197200,1646283600,1646370000,1646456400,1646542800],"daypart":[{"cloudCover":[94,92,83,84,73,61,57,59,67,66,68,69,59,68,64,64,49,62,52,72,57,68,56,53,66,52,68,70,60,62],"dayOrNight":["D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N"],"daypartName":["Today","Tonight","Tomorrow","Tomorrow night","Tuesday","Tuesday night","Wednesday","Wednesday night","Thursday","Thursday night","Friday","Friday night","Saturday","Saturday night","Sunday","Sunday night","Monday","Monday night","Tuesday","Tuesday night","Wednesday","Wednesday night","Thursday","Thursday night","Friday","Friday night","Saturday","Saturday night","Sunday","Sunday night"],"iconCode":[38,11,4,47,38,47,38,47,4,47,4,47,38,47,38,29,38,29,38,47,38,47,38,47,4,4,4,4,38,11],"iconCodeExtend":[7203,1140,400,3809,3800,6200,7203,3809,400,3809,400,3809,3800,3809,3800,2900,3800,2900,3800,3809,3800,3809,3800,3809,400,400,400,400,3800,1100],"narrative":["Thunderstorms developing in the afternoon. High 21ºC. Winds WSW at 10 to 15 km/h. Chance of rain 40%.","Thundershowers. Low 17ºC. Winds WSW and variable. Chance of rain 40%.","Thunderstorms. High 22ºC. Winds SW at 10 to 15 km/h. Chance of rain 80%.","Scattered thunderstorms. Low 17ºC. Winds SW and variable. Chance of rain 50%.","Scattered thunderstorms. High 24ºC. Winds S at 10 to 15 km/h. Chance of rain 50%.","Thunderstorms early. Low 17ºC. Winds S and variable. Chance of rain 40%.","Thunderstorms developing in the afternoon. High 24ºC. Winds S at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 16ºC. Winds SSE and variable. Chance of rain 40%.","Thunderstorms. High 24ºC. Winds S and variable. Chance of rain 70%.","Scattered thunderstorms. Low 17ºC. Winds NW and variable. Chance of rain 60%.","Thunderstorms. High 24ºC. Winds WNW and variable. Chance of rain 70%.","Scattered thunderstorms. Low 17ºC. Winds SW and variable. Chance of rain 60%.","Scattered thunderstorms. High 24ºC. Winds SSW at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SE and variable. Chance of rain 50%.","Scattered thunderstorms. High 24ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Partly cloudy. Low 17ºC. Winds SE and variable.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 40%.","Partly cloudy. Low 17ºC. Winds ESE and variable.","Scattered thunderstorms. High 25ºC. Winds SE at 10 to 15 km/h. Chance of rain 60%.","Scattered thunderstorms. Low 17ºC. Winds SE and variable. Chance of rain 50%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 50%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 50%.","Thunderstorms. High 24ºC. Winds SSE at 10 to 15 km/h. Chance of rain 60%.","Thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 60%.","Thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 60%.","Thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 60%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Showers. Low 16ºC. Winds SSE and variable. Chance of rain 50%."],"precipChance":[40,42,78,47,51,43,45,42,68,55,74,58,53,53,54,24,44,24,57,51,53,51,50,45,60,60,60,60,51,50],"precipType":["rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain"],"qpf":[0.3,1.34,6.46,1.54,2.6,0.86,0.84,0.8,4.73,2.56,4.5,1.26,2.76,1.2,3,0,1.56,0,3.3,1.54,2.6,1.4,2.9,1.3,3.24,1.53,4.03,1.4,2.9,1.15],"qpfSnow":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"qualifierCode":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"qualifierPhrase":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"relativeHumidity":[90,96,87,94,79,95,78,94,81,94,80,95,80,96,79,96,76,94,75,93,74,93,75,94,77,94,77,94,77,96],"snowRange":["","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""],"temperature":[21,17,22,17,24,17,24,16,24,17,24,17,24,17,24,17,25,17,25,17,25,17,25,17,24,17,25,17,25,16],"temperatureHeatIndex":[22,20,23,21,25,21,24,20,24,21,24,21,24,21,25,21,27,21,27,21,26,21,25,21,25,21,25,21,25,20],"temperatureWindChill":[20,18,18,18,19,17,18,17,18,17,18,17,18,17,18,17,18,17,18,17,19,17,18,17,18,17,18,17,18,17],"thunderCategory":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"thunderIndex":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,2,0,2,2,2,2,2,2,2,2,2,2,2,0],"uvDescription":["High","Low","Very High","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low"],"uvIndex":[7,0,10,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0],"windDirection":[240,250,232,228,189,183,171,157,171,326,295,229,195,145,163,139,156,113,146,124,150,149,156,162,158,155,150,154,152,160],"windDirectionCardinal":["WSW","WSW","SW","SW","S","S","S","SSE","S","NW","WNW","SW","SSW","SE","SSE","SE","SSE","ESE","SE","SE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE"],"windPhrase":["Winds WSW at 10 to 15 km/h.","Winds WSW and variable.","Winds SW at 10 to 15 km/h.","Winds SW and variable.","Winds S at 10 to 15 km/h.","Winds S and variable.","Winds S at 10 to 15 km/h.","Winds SSE and variable.","Winds S and variable.","Winds NW and variable.","Winds WNW and variable.","Winds SW and variable.","Winds SSW at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds ESE and variable.","Winds SE at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable."],"windSpeed":[12,7,14,6,15,8,14,9,9,5,10,5,12,7,11,6,10,7,10,6,10,7,12,7,12,7,11,7,12,7],"wxPhraseLong":["PM T-Storms","T-Showers","T-Storms","Scattered T-Storms","Scattered T-Storms","T-Storms Early","PM T-Storms","Scattered T-Storms","T-Storms","Scattered T-Storms","T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Partly Cloudy","Scattered T-Storms","Partly Cloudy","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","T-Storms","T-Storms","T-Storms","T-Storms","Scattered T-Storms","Showers"],"wxPhraseShort":["","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]}]} - -const apiRequestLimit = 50; // It's really 100 per minute -let apiRequestCounter = 0; - -export class WeatherCompanyAPI { - - defaultOptions: CommonOptions; - - baseAPI = "https://api.weather.com/" - - constructor(private apiKey = process.env.weather_company_apiKey as string, private unit = Units.metric, private language = Languages.English, private format = Formats.JSON) { - if (apiKey == undefined) { - console.error("Weather Company API isn't defined!"); - console.error("Please set it on the 'weather_company_apiKey' env variable or pass it to the constructor"); - throw new Error("Please pass an API key to the Weather API"); - } - - this.defaultOptions = { - format: Formats.JSON, - language, - units: unit - } - - // setup API throttler, every minute it resets the apiRequestLimit - setInterval(() => { - apiRequestCounter = 0; - }, 1000 * 60); - } - - GeoCodeToString(geocode: GeoCode) { - return `${geocode.latitude},${geocode.longitude}` - } - - apiHitCounter() { - if (apiRequestCounter >= apiRequestLimit) { - throw new Error("Too many Requests"); - } - else { - apiRequestCounter++; - } - } - - async daily15DayForecast(geocode: GeoCode, commonOptions = this.defaultOptions) { - const paramOptions = { - geocode: this.GeoCodeToString(geocode), - apiKey: this.apiKey - } - const queryOptions = Object.assign({}, commonOptions, paramOptions); - - this.apiHitCounter() - - const response = await axios.get(this.baseAPI + "v3/wx/forecast/daily/15day", { - params: queryOptions - }); - - return response.data; - - // return testForecastData; - } -} diff --git a/backend/src/integrations/weatherCompany/WeatherCompanyService.ts b/backend/src/integrations/weatherCompany/WeatherCompanyService.ts new file mode 100644 index 000000000..787a8b336 --- /dev/null +++ b/backend/src/integrations/weatherCompany/WeatherCompanyService.ts @@ -0,0 +1,53 @@ +import axios from "axios"; +import { CommonOptions, DataFormat, GeoCodeNumber, geoCodeToString, Language, Unit, WeatherCompanyConfig } from "common-types"; + +const apiRequestLimit = 50; // It's really 100 per minute +let apiRequestCounter = 0; + +class WeatherCompanyService { + + defaultOptions: CommonOptions; + + // baseAPI = "https://api.weather.com/" + + constructor() { + this.defaultOptions = { + format: DataFormat.JSON, + language: Language.English, + units: Unit.metric + } + + // setup API throttler, every minute it resets the apiRequestLimit + setInterval(() => { + apiRequestCounter = 0; + }, 1000 * 60); + } + + apiHitCounter() { + if (apiRequestCounter >= apiRequestLimit) { + throw new Error("Too many Requests"); + } + else { + apiRequestCounter++; + } + } + + async daily15DayForecast(config: WeatherCompanyConfig, geocode: GeoCodeNumber) { + const commonOptions = config.options || this.defaultOptions; + const paramOptions = { + geocode: geoCodeToString(geocode), + apiKey: config.apiKey + } + const queryOptions = Object.assign({}, commonOptions, paramOptions); + + this.apiHitCounter() + + const response = await axios.get(config.apiUrl + "v3/wx/forecast/daily/15day", { + params: queryOptions + }); + + return response.data; + } +} + +export const weatherCompanyService: WeatherCompanyService = new WeatherCompanyService(); diff --git a/common-types/Fields.ts b/backend/src/integrations/weatherCompany/weather-company-api.types.ts similarity index 100% rename from common-types/Fields.ts rename to backend/src/integrations/weatherCompany/weather-company-api.types.ts diff --git a/backend/src/main.ts b/backend/src/main.ts index 9dea7d38c..440fc7a67 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -17,14 +17,14 @@ import cropRoutes from "./routes/crop-route"; import dashboardRoutes from "./routes/dashboard-route"; import recommendationsRoutes from "./routes/recommendations-route"; import weatherRoutes from "./routes/weather-route"; -import coopManagerRoutes from "./routes/coopManager-route"; +import userRoutes from "./routes/user-route"; import organisationRoutes from "./routes/organisation-route"; import messageLogRoutes from "./routes/messaging-route"; import smsRoutes from "./routes/sms-route"; -import { formatUser, ensureAuthenticated } from "./auth/helpers"; +import { ensureAuthenticated, formatUser } from "./auth/helpers"; import { IBMidStrategy } from "./auth/IBMiDStrategy"; -import { SocketIOManager, SocketIOManagerInstance } from "./sockets/socket.io"; +import { SocketIOManagerInstance } from "./sockets/socket.io"; import { Server } from "http"; mongoInit(); @@ -118,7 +118,7 @@ app.use("/api/dashboard", dashboardRoutes); app.use("/api/recommendations", recommendationsRoutes); app.use("/api/weather", weatherRoutes); -app.use("/api/coopManager", coopManagerRoutes); +app.use("/api/coopManager", userRoutes); app.use("/api/organisation", organisationRoutes); app.use("/api/messaging", messageLogRoutes); app.use("/api/sms", smsRoutes); diff --git a/backend/src/routes/coopManager-route.ts b/backend/src/routes/coopManager-route.ts deleted file mode 100644 index 4253813bc..000000000 --- a/backend/src/routes/coopManager-route.ts +++ /dev/null @@ -1,90 +0,0 @@ -// import dependencies and initialize the express router -import { Router } from "express"; -import { getOrganisations } from "./../services/organisation.service"; -import { addCoopManagerToOrganisation, doesUserExist, getCoopManager, onBoardUser } from "./../services/coopManager.service"; - -const router = Router(); - -// define routes -router.get(":id", async (req, res) => { - const id = req.params.id; - const manager = await getCoopManager(id); - if (manager !== null) { - return res.json(manager.toObject()); - } - else { - return res.status(404).end(); - } -}); - -router.get("/hasBeenOnBoarded", async (req, res) => { - const prefix = "IBMid:"; - if (req.user === undefined) { - return res.status(400).send("User hasn't logged in."); - } - const id = `${prefix}${req.user.id}`; - const result = await doesUserExist(id); - res.json({exists: result}); -}); - -router.post("/onboard", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.oAuthSource === undefined) { - return res.status(400).send("oAuthSource is missing"); - } - if (req.body.oAuthId === undefined) { - return res.status(400).send("oAuthId is missing"); - } - if (req.body.user === undefined) { - return res.status(400).send("user (Coop Manager) is missing"); - } - - const userDoc = await onBoardUser(req.body.oAuthSource, req.body.oAuthId, req.body.user); - - // Set the Organisation variables on the user - req.user.isOnboarded = true; - req.user.coopManager = userDoc.toObject(); - req.user.organisations = await getOrganisations(userDoc.coopOrganisations); - req.user.selectedOrganisation = req.user.organisations[0]; - - res.json(userDoc.toObject()); -}); - -router.put("/setCurrentOrganisation", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.orgId === undefined) { - return res.status(400).send("orgId is missing"); - } - const orgId = req.body.orgId; - const org = req.user.organisations.find(it => it._id == orgId); - if (org == undefined) { - return res.status(400).json("User is not part of organisation"); - } - - req.user.selectedOrganisation = org; - - res.json(org); -}) - -router.put("/:id/addOrganisation", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.orgId === undefined) { - return res.status(400).send("coopManagerId is missing"); - } - const orgId = req.body.orgId; - const coopManagerId = req.params.id; - const coopUser = await addCoopManagerToOrganisation(coopManagerId, orgId); - - req.user.coopManager = coopUser.toObject(); - req.user.organisations = await getOrganisations(coopUser.coopOrganisations, true); - - res.json(coopUser.toObject()); -}); - -export default router; diff --git a/backend/src/routes/crop-route.ts b/backend/src/routes/crop-route.ts index 0b6ad622a..5fd471e92 100644 --- a/backend/src/routes/crop-route.ts +++ b/backend/src/routes/crop-route.ts @@ -1,7 +1,7 @@ import { Request, Response, Router } from "express"; -import CropService from "../services/crop.service"; -var router = Router(); -const cropService = new CropService(); +import { cropService } from "../services/CropService"; + +const router = Router(); router.get("/", getAllCrops); diff --git a/backend/src/routes/dashboard-route.ts b/backend/src/routes/dashboard-route.ts index c5c00f016..d202ed157 100644 --- a/backend/src/routes/dashboard-route.ts +++ b/backend/src/routes/dashboard-route.ts @@ -1,7 +1,8 @@ import { Router } from "express"; +import LandAreasService from "../services/land-areas.service"; + var router = Router(); -import LandAreasService from "../services/land-areas.service"; const lotAreas = new LandAreasService(); //data table diff --git a/backend/src/routes/farmer-route.ts b/backend/src/routes/farmer-route.ts index 35ee77091..925cb51ab 100644 --- a/backend/src/routes/farmer-route.ts +++ b/backend/src/routes/farmer-route.ts @@ -1,27 +1,18 @@ -import { Router, Request, Response } from "express"; -import { EISField } from "../integrations/EIS/EIS.types"; -import { EISAPIService } from "../integrations/EIS/EIS-api.service"; -import { Farmer, FarmerModel } from "../db/entities/farmer"; -import LandAreasService from "../services/land-areas.service"; +import { Request, Response, Router } from "express"; +import { farmerService } from "../services/FarmerService"; + +import { Farmer, isUndefined, NewFarmer } from "common-types"; +import { FarmerModel } from "../db/entities/farmer"; +import { farmService } from "../services/FarmService"; // const LotAreaService = require("./../services/lot-areas.service"); // const lotAreas = new LandAreasService(); -const EISKey = process.env.EIS_apiKey; - -if (EISKey == undefined) { - console.error("You must define 'EIS_apiKey' in the environment!"); - process.exit(-1); -} - -const eisAPIService = new EISAPIService(EISKey); - const router = Router(); router.get("/", async (req: Request, res: Response) => { try { - const docs = await FarmerModel.find().lean().exec(); - res.json(docs); + res.json(farmerService.getFarmers()); } catch (e) { console.error(e); res.status(500).json(e); @@ -35,9 +26,7 @@ async function createOrUpdateFarmer(req: Request, res: Response) { return; } try { - const farmerDoc = new FarmerModel(farmer); - const updatedDoc = farmerDoc.save(); - res.json(updatedDoc); + res.json(farmerService.saveFarmer(farmer)); } catch (e) { console.error(e); res.status(500).json(e); @@ -45,17 +34,8 @@ async function createOrUpdateFarmer(req: Request, res: Response) { } -async function getFarmer(id: string) { - // Aggregate with land areas eventually - const farmer = await FarmerModel.findById(id).lean().exec(); - if (farmer == null) { - return null; - } - - // Get Fields - const field = await eisAPIService.getFarmerField(id); - - return farmer; +async function getFarmer(id: string): Promise { + return farmerService.getFarmer(id); } router.post("/", createOrUpdateFarmer); @@ -70,7 +50,7 @@ router.get("/:id", async (req: Request, res: Response) => { } try { - const farmer = getFarmer(id); + const farmer = await getFarmer(id); if (farmer == null) { res.status(404).end(); } @@ -92,7 +72,7 @@ router.delete("/:id", async(req: Request, res: Response) => { return; } try { - const result = await FarmerModel.deleteOne({_id: id}); + const result = await FarmerModel.findByIdAndDelete(id); res.json(result); } catch (e) { console.error(e); @@ -100,48 +80,23 @@ router.delete("/:id", async(req: Request, res: Response) => { } }); -export interface FarmerAddDTO { - farmer: Farmer; - field: EISField -} -router.post("/add", async(req: Request, res: Response) => { - const {farmer, field}: FarmerAddDTO = req.body; - if (farmer == undefined) { - res.status(400).send("Farmer not defined"); + +router.post("/add", async(req: Request<{}, {}, NewFarmer>, res: Response) => { + const newFarmer: NewFarmer = req.body; + if (isUndefined(newFarmer)) { + res.status(400).send("Farmer is not defined"); return; } - if (field == undefined) { - res.status(400).send("Field not defined"); + if (isUndefined(newFarmer.farms)) { + res.status(400).send("Farm is not defined"); return; } - // First we'll create the farmer - const farmerDoc = new FarmerModel(farmer); - const newFarmer = await farmerDoc.save(); - - if (newFarmer._id == undefined) { - throw new Error("Farmer ID is not defined after saving!") - } - - // Then we'll create the Field - - // We have to set the farmer ID on the field first - for (let i = 0; i < field.subFields.length; i++) { - const properties = field.subFields[i].geo.geojson.features[0].properties; - properties.open_harvest_farmer_id = newFarmer._id!!.toString(); - properties.open_harvest.farmer_id = newFarmer._id!!.toString(); - } - - const createdFieldsUuids = await eisAPIService.createField(field); - const fieldUuid = createdFieldsUuids.field; - - const createdField = await eisAPIService.getField(fieldUuid); - - const farmerObj = newFarmer.toObject(); + const farmer = await farmerService.saveFarmer(newFarmer); - farmerObj.field = createdField; + const farms = await farmService.saveFarms(farmer, newFarmer.farms); - res.json(farmerObj); + res.json(farmer); }); // // Link Lot diff --git a/backend/src/routes/lot-route.ts b/backend/src/routes/lot-route.ts index 7320379f3..aef5e3c07 100644 --- a/backend/src/routes/lot-route.ts +++ b/backend/src/routes/lot-route.ts @@ -1,7 +1,8 @@ -import { Router, Request, Response } from "express"; +import { Request, Response, Router } from "express"; +import LandAreasService from "../services/land-areas.service"; + var router = Router(); -import LandAreasService from "../services/land-areas.service"; const lotAreas = new LandAreasService(); router.get("/", getAllLots); diff --git a/backend/src/routes/messaging-route.ts b/backend/src/routes/messaging-route.ts index dd99e5177..bcef68094 100644 --- a/backend/src/routes/messaging-route.ts +++ b/backend/src/routes/messaging-route.ts @@ -1,6 +1,6 @@ // import dependencies and initialize the express router import { Router } from "express"; -import { TwilioInstance } from "./../integrations/twilio/twilio.service"; +import { TwilioAPI } from "../integrations/twilio/twilio.service"; import { MessageLogModel } from "../db/entities/messageLog"; import { FarmerModel } from "../db/entities/farmer"; @@ -22,7 +22,7 @@ router.post("/sendSMSToFarmer", async (req, res) => { } try { - const messageLog = await TwilioInstance.sendMessageToFarmer(farmer, message); + const messageLog = await new TwilioAPI().sendMessageToFarmer(farmer, message); res.json(messageLog) } catch (e: any) { diff --git a/backend/src/routes/organisation-route.ts b/backend/src/routes/organisation-route.ts index 63f654e5e..222a0a36b 100644 --- a/backend/src/routes/organisation-route.ts +++ b/backend/src/routes/organisation-route.ts @@ -1,11 +1,12 @@ // import dependencies and initialize the express router -import { Router } from "express"; -import { createOrganisationFromName, getAllOrganisations, getOrganisation, getOrganisations } from "./../services/organisation.service"; +import { isDefined, isUndefined, OrganisationDto } from "common-types"; +import { Request, Router } from "express"; +import { organisationService } from "../services/OrganisationService"; const router = Router(); router.get("/", async (req, res) => { - const orgs = await getAllOrganisations(true); + const orgs = await organisationService.getAllOrganisations(true); // console.log(orgs); return res.json(orgs); }); @@ -13,41 +14,33 @@ router.get("/", async (req, res) => { // define routes router.get("/:id", async (req, res) => { const id = req.params.id; - const org = await getOrganisation(id); + const org = await organisationService.getOrganisation(id); if (org == null) { return res.sendStatus(404); } else { - return res.json(org.toObject()); + return res.json(org); } }); -router.post("/", async (req, res) => { - if (req.body === undefined) { +router.post("/", async (req: Request<{}, {}, OrganisationDto>, res) => { + if (isUndefined(req.body)) { return res.status(400).send("Body is missing"); } - if (req.body.name === undefined) { + if (isUndefined(req.body.name)) { return res.status(400).send("name is missing"); } const name = req.body.name; - console.log("Creating Org:", name); - const doc = await createOrganisationFromName(name); - res.json(doc.toObject()); -}); -router.get("/my", async (req, res) => { - if (req.user == undefined) { - return res.status(401); - } + const organisation = await organisationService.getOrganisation(name) - const isOnboarded = req.user.isOnboarded; - if (!isOnboarded) { - return res.status(400).json({error: "user is not onboarded"}); + if (isDefined(organisation)) { + return res.status(409).send("Organisation already exists: " + name); } - - // Get the organisations of the user - const orgs = await getOrganisations(req.user, true); - return orgs; + console.log("Creating Org:", name); + const doc = await organisationService.createOrganisation(req.body); + res.json(doc); }); + export default router; diff --git a/backend/src/routes/recommendations-route.ts b/backend/src/routes/recommendations-route.ts index 11554a5cf..9ba82126c 100644 --- a/backend/src/routes/recommendations-route.ts +++ b/backend/src/routes/recommendations-route.ts @@ -1,7 +1,8 @@ import { Router } from "express"; +import RecommendationsService from "../services/recommendations.service"; + var router = Router(); -import RecommendationsService from "../services/recommendations.service"; const recommendationsService = new RecommendationsService(); router.post("/", async(req, res) => { try { diff --git a/backend/src/routes/sms-route.ts b/backend/src/routes/sms-route.ts index 89ba26a9f..0997038bd 100644 --- a/backend/src/routes/sms-route.ts +++ b/backend/src/routes/sms-route.ts @@ -4,9 +4,8 @@ import { Router } from "express"; -import { MessageLogModel } from "../db/entities/messageLog"; -import { SMSSyncAPIInstance, SMSSyncMessageReceivedFormat } from "./../integrations/smsSync/smsSync.service"; -import { TwilioInstance, TwilioMessage } from "../integrations/twilio/twilio.service"; +import { SMSSyncAPIInstance, SMSSyncMessageReceivedFormat } from "../integrations/smsSync/smsSync.service"; +import { TwilioAPI, TwilioMessage } from "../integrations/twilio/twilio.service"; const router = Router(); @@ -53,7 +52,7 @@ router.post("/twilio-sms-incoming", async (req, res) => { // console.log("Twilio Message:", req.body); const message: TwilioMessage = req.body; - TwilioInstance.onReceivedMessage(message); + new TwilioAPI().onReceivedMessage(message); res.status(200).end(); }); diff --git a/backend/src/routes/user-route.ts b/backend/src/routes/user-route.ts new file mode 100644 index 000000000..805463ed9 --- /dev/null +++ b/backend/src/routes/user-route.ts @@ -0,0 +1,52 @@ +import { Request, Router } from "express"; +import { organisationService } from "../services/OrganisationService"; +import { userService } from "../services/UserService"; +import { isUndefined, OrganisationDto, UserDto } from "common-types"; + +const router = Router(); + +// define routes +router.get(":id", async (req, res) => { + const id = req.params.id; + const users = await userService.getUser(id); + if (isUndefined(users) || users.length === 0) { + return res.status(404).end(); + } + + return res.json(users); +}); + +router.post("/hasBeenOnBoarded", async (req: Request<{}, {}, UserDto>, res) => { + if (req.body === undefined) { + return res.status(400).send("User hasn't logged in."); + } + const result = await userService.doesUserExist(req.body); + res.json({exists: result}); +}); + +router.post("/onboard", async (req: Request<{}, {}, UserDto>, res) => { + let userDto = req.body; + if (userDto === undefined) { + return res.status(400).send("Body is missing"); + } + if (userDto.organisation === undefined) { + return res.status(400).send("organisation is missing"); + } + + + const userDoc = await userService.onBoardUser(userDto); + res.json(userDoc); +}); + + +router.put("/addOrganisation", async (req: Request<{}, {}, OrganisationDto>, res) => { + if (req.body === undefined) { + return res.status(400).send("Body is missing"); + } + + const org = await organisationService.createOrganisation(req.body); + + res.json(org); +}); + +export default router; diff --git a/backend/src/routes/weather-route.ts b/backend/src/routes/weather-route.ts index 29dacd0b4..138d98b00 100644 --- a/backend/src/routes/weather-route.ts +++ b/backend/src/routes/weather-route.ts @@ -1,24 +1,34 @@ // import dependencies and initialize the express router -import { Router } from "express"; -import { GeoCode } from "integrations/weather-company-api.types"; -import { WeatherCompanyAPI } from "./../integrations/weather-company-api.service"; +import { Request, Router } from "express"; +import { isUndefined, toGroCodeFromPoint, UserDto } from "common-types"; +import { weatherCompanyService } from "../integrations/weatherCompany/WeatherCompanyService"; +import { organisationService } from "../services/OrganisationService"; const router = Router(); -const api = new WeatherCompanyAPI(); - -const testMchinjiMalawiCoords: GeoCode = { - latitude: -13.7971726, - longitude: 32.8874963 -} +// const testMchinjiMalawiCoords: GeoCode = { +// latitude: -13.7971726, +// longitude: 32.8874963 +// } /** * Gets the forecast for a farmer. Right now this is hardcoded while we wait for farmer data from the session */ -router.get("/farmerForecast", async (req, res) => { +router.post("/farmerForecast", async (req: Request<{}, {}, UserDto>, res) => { console.log("farmerForecast"); - const geocode = testMchinjiMalawiCoords; - const forecast = await api.daily15DayForecast(geocode); + + const userDto = req.body; + const organisation = await organisationService.getOrganisation(userDto.organisation); + + if (isUndefined(organisation)) { + throw new Error("Organisation does not exist: " + userDto.organisation) + } + + if (isUndefined(organisation.weatherCompanyConfig)) { + return res.status(400).send( "User's organisation is not configured to use weather company"); + } + + const forecast = await weatherCompanyService.daily15DayForecast(organisation.weatherCompanyConfig, toGroCodeFromPoint(userDto.location)); // console.log(forecast); res.json(forecast); }); diff --git a/backend/src/services/crop.service.ts b/backend/src/services/CropService.ts similarity index 70% rename from backend/src/services/crop.service.ts rename to backend/src/services/CropService.ts index d2e49a25f..b7e623a64 100644 --- a/backend/src/services/crop.service.ts +++ b/backend/src/services/CropService.ts @@ -2,15 +2,10 @@ // const {cropDetailsView} = require("../db/cloudant"); // const {cropDetailsDdoc} = require("../db/cloudant"); -import { CropModel, Crop } from "../db/entities/crop"; +import { Crop } from "common-types"; +import { CropModel } from "../db/entities/crop"; -// const APPLICATION_DB = "application-db"; -// const db = APPLICATION_DB; - -// const LOT_DB = "lot-areas"; -let cropDetails; - -export default class CropService { +export class CropService { constructor() { } @@ -36,4 +31,4 @@ export default class CropService { } } -// module.exports = CropService; +export const cropService = new CropService(); diff --git a/backend/src/services/FarmService.ts b/backend/src/services/FarmService.ts new file mode 100644 index 000000000..07e3902c9 --- /dev/null +++ b/backend/src/services/FarmService.ts @@ -0,0 +1,64 @@ +import { EISConfig, Farm, Farmer, isDefined, isUndefined, NewFarm, Organisation } from "common-types"; +import { FarmModel } from "../db/entities/farm"; +import { eisFarmService } from "../integrations/EIS/EISFarmService"; +import { organisationService } from "./OrganisationService"; + +export interface IFarmService { + getFarmerFarms(farmer: Farmer): Promise; + saveFarm(newFarm: NewFarm): Promise; + saveFarms(farmer: Farmer, newFarms: NewFarm[]): Promise; +} + +class FarmService implements IFarmService { + + async getFarmerFarms(farmer: Farmer): Promise { + const eisConfig = await FarmService.getEISConfig(farmer.organisation); + + if (isDefined(eisConfig)) { + return await eisFarmService.getFarmerFarms(eisConfig, farmer); + } + + return await FarmModel.find({"farmer._id": farmer._id}).exec() as Farm[]; + } + + async saveFarm(newFarm: NewFarm): Promise { + + const eisConfig = await FarmService.getEISConfig(newFarm.farmer.organisation); + + if (isDefined(eisConfig)) { + return await eisFarmService.saveFarm(eisConfig, newFarm); + } + + const farmDoc = new FarmModel(newFarm); + return await farmDoc.save() as Farm; + } + + async saveFarms(farmer: Farmer, newFarms: NewFarm[]): Promise { + const eisConfig = await FarmService.getEISConfig(farmer.organisation); + const farms: Farm[] = []; + for (const newFarm of newFarms) { + newFarm.farmer = farmer; + let farm: Farm; + if (isDefined(eisConfig)) { + farm = await eisFarmService.saveFarm(eisConfig, newFarm); + } else { + farm = await this.saveFarm(newFarm) as Farm; + } + farms.push(farm); + } + + return farms; + } + + private static async getEISConfig(org: string): Promise { + let organisation: Organisation | null = await organisationService.getOrganisation(org); + if (isUndefined(organisation)) { + throw new Error("Organisation does not exist: " + org) + } + return organisation.eisConfig; + } + +} + +export const farmService = new FarmService(); + diff --git a/backend/src/services/FarmerService.ts b/backend/src/services/FarmerService.ts new file mode 100644 index 000000000..73d55c921 --- /dev/null +++ b/backend/src/services/FarmerService.ts @@ -0,0 +1,32 @@ +import { FarmerModel } from "../db/entities/farmer"; +import { Farmer, NewFarmer } from "common-types"; +import { farmService } from "./FarmService"; + +class FarmerService { + async getFarmers(): Promise { + return await FarmerModel.find().lean().exec(); + } + + async saveFarmer(newFarmer: NewFarmer): Promise { + const farmerDoc = new FarmerModel(newFarmer); + return await farmerDoc.save(); + } + + async getFarmer(id: string): Promise { + const farmer = await FarmerModel.findById(id).lean().exec(); + if (farmer == null) { + return null; + } + + // Get Fields + const farms = await farmService.getFarmerFarms(farmer); + + return { + ...farmer, + farms + }; + } +} + + +export const farmerService = new FarmerService() diff --git a/backend/src/services/OrganisationService.ts b/backend/src/services/OrganisationService.ts new file mode 100644 index 000000000..dbe0a5a6c --- /dev/null +++ b/backend/src/services/OrganisationService.ts @@ -0,0 +1,63 @@ +import { isDefined, isUndefined, Organisation, OrganisationDto, User, UserDto } from "common-types"; +import { OrganisationModel } from "../db/entities/organisation"; +import { createToUserDto } from "./UserService"; + + +class OrganisationService { + + async getOrganisationsByUserId(userId: string): Promise { + return await OrganisationModel.find({ + "users._id": userId + } + ).exec(); + }; + + async getAllOrganisations(lean = true): Promise { + if (lean) + return OrganisationModel.find({}).lean(); + else + return OrganisationModel.find({}); + } + + async getOrganisation(id: string): Promise { + return OrganisationModel.findById(id); + } + + async createOrganisation(org: OrganisationDto): Promise { + const organisation = new OrganisationModel(); + + organisation.authMethod = org.authMethod; + organisation.users = org.users.map(userDto => { + const user: User = { + location: userDto.location, + mobile: userDto.mobile + } + return user; + }); + return organisation.save(); + } + + async addUserToOrganisation(userDto: UserDto): Promise { + const org = await OrganisationModel.findById(userDto.organisation) + if (isUndefined(org)) { + throw new Error("Organisation does not exist: " + userDto.organisation); + } + + let orgUser = org.users.find(orgUser => orgUser._id === user._id); + if (isDefined(orgUser)) { + return createToUserDto(orgUser, org); + } + + const user: User = { + location: userDto.location, + _id: `${org.authMethod}:${userDto.id}`, + mobile: userDto.mobile + } + + org.users.push(user); + await org.save(); + return createToUserDto(user, org); + } +} + +export const organisationService = new OrganisationService(); diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts new file mode 100644 index 000000000..4832a20ba --- /dev/null +++ b/backend/src/services/UserService.ts @@ -0,0 +1,54 @@ +import { isUndefined, NewUserDto, Organisation, User, UserDto } from "common-types"; +import { organisationService } from "./OrganisationService"; + + +class UserService { + + async getUser(userId: string): Promise { + const orgs: Organisation[] = await organisationService.getOrganisationsByUserId(userId); + + if (isUndefined(orgs) || orgs.length === 0) { + return []; + } + + const userDtos: UserDto[] = []; + + orgs.forEach(org => { + org.users.filter(user => user._id === userId).map(user => { + userDtos.push(createToUserDto(user, org)); + }) + }); + + return userDtos; + } + + /** + * Explicit use of the word user here because it's the OAuth User we're talking about. + * provider + id + * e.g. "IBMid:1SD54A1" + */ + async doesUserExist(userDto: UserDto): Promise { + return (await this.getUser(userDto.id)) != null + } + + async onBoardUser(userDto: NewUserDto): Promise { + // Check if the organisation exists + return await organisationService.addUserToOrganisation(userDto); + + } +} + +export const userService = new UserService(); + +export function createToUserDto(user: User, org: Organisation): UserDto { + if (isUndefined(user._id)) { + throw new Error("User does not have id.") + } + return { + email: "", + location: user.location, + mobile: user.mobile, + organisation: org.name, + id: user._id + }; +} diff --git a/backend/src/services/coopManager.service.ts b/backend/src/services/coopManager.service.ts deleted file mode 100644 index befbd436b..000000000 --- a/backend/src/services/coopManager.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CoopManagerModel, CoopManager } from "./../db/entities/coopManager"; -import { OrganisationModel, Organisation } from "./../db/entities/organisation"; - -export function getCoopManager(id: string) { - return CoopManagerModel.findById(id); -} - -/** - * Explicit use of the word user here because it's the OAuth User we're talking about. - * provider + id - * e.g. "IBMid:1SD54A1" - */ -export async function doesUserExist(id: string) { - const manager = await CoopManagerModel.findById(id); - return manager !== null; -} - -export async function onBoardUser(oAuthSource: string, oAuthId: string, user: CoopManager) { - // Check if the organisation exists - const orgs = await OrganisationModel.find({ - _id: { - $in: [user.coopOrganisations] - } - }); - if (orgs.length !== user.coopOrganisations.length) { - throw new Error("Organisation's given weren't found!"); - } - const newID = `${oAuthSource}:${oAuthId}` - user._id = newID; - const userDoc = await CoopManagerModel.create(user); - return userDoc; -} - -export async function addCoopManagerToOrganisation(coopManagerId: string, orgId: string) { - // Check if the Coop Manager exists - const coopManager = await CoopManagerModel.findById(coopManagerId); - if (coopManager == null) { - throw new Error("Coop Manager doesn't exist!"); - } - const org = await OrganisationModel.findById(orgId); - if (org == null) { - throw new Error("Organisation doesn't exist!"); - } - - if (coopManager.coopOrganisations.includes(coopManagerId)) { - return coopManager; - } - else { - coopManager.coopOrganisations.push(orgId); - const newCoopManager = await coopManager.save(); - return newCoopManager; - } -} - -// module.exports = CropService; diff --git a/backend/src/services/land-areas.service.ts b/backend/src/services/land-areas.service.ts index 17533015c..50ea97e0d 100644 --- a/backend/src/services/land-areas.service.ts +++ b/backend/src/services/land-areas.service.ts @@ -1,6 +1,4 @@ -import { Land, LandModel } from "../db/entities/land"; -import { Types } from 'mongoose'; -import { FarmerModel } from "./../db/entities/farmer"; +// import { Land, LandModel } from "../db/entities/land"; // const nswBbox = "140.965576,-37.614231,154.687500,-28.071980"; // lng lat // const nswBboxLatLng = "-37.614231,140.965576,-28.071980,154.687500"; // lat lng @@ -17,24 +15,24 @@ export interface BoundingBox { export default class LandAreasService { constructor() {} - async updateLot(lot: Land) { - const landModel = new LandModel(lot); - - const savedDoc = await landModel.save(); - return savedDoc; - } - - getLot(id: string) { - return LandModel.findById(id); - } - - getLots(ids: string[]) { - return LandModel.find({ '_id': { $in: ids } }); - } - - getAllLots() { - return LandModel.find(); - } + // async updateLot(lot: Land) { + // const landModel = new LandModel(lot); + // + // const savedDoc = await landModel.save(); + // return savedDoc; + // } + + // getLot(id: string) { + // return LandModel.findById(id); + // } + // + // getLots(ids: string[]) { + // return LandModel.find({ '_id': { $in: ids } }); + // } + // + // getAllLots() { + // return LandModel.find(); + // } getAreasInBbox(box: BoundingBox) { // const bbox = `${box.lowerLeft.lng},${box.lowerLeft.lat},${box.upperRight.lng},${box.upperRight.lat}`; @@ -162,14 +160,10 @@ export default class LandAreasService { return 0; } - getTotalFarmers() { - //return this.getViewValue(farmerCountDoc, farmerCountView, APPLICATION_DB); - return FarmerModel.count().exec(); - } getCropsPlanted() { // return this.getViewValue(cropsPlantedDoc, cropsPlantedView, LOT_DB); - + // Aggregate of crops planted return 0; } @@ -181,9 +175,9 @@ export default class LandAreasService { return 0; } - getTotalLots() { - return LandModel.count().exec(); - } + // getTotalLots() { + // return LandModel.count().exec(); + // } } // module.exports = LotAreas; diff --git a/backend/src/services/organisation.service.ts b/backend/src/services/organisation.service.ts deleted file mode 100644 index ab9dd6d22..000000000 --- a/backend/src/services/organisation.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { OrganisationModel, Organisation } from "./../db/entities/organisation"; -import { CoopManagerModel, CoopManager } from "./../db/entities/coopManager"; - -export function getAllOrganisations(lean = true) { - if (lean) - return OrganisationModel.find({}).lean(); - else - return OrganisationModel.find({}); -} - -export function getOrganisation(id: string) { - return OrganisationModel.findById(id); -} - -export function getOrganisations(id: string[], lean = true) { - const query = OrganisationModel.find({_id: {$in: id}}); - return lean ? query.lean() : query; -} - -export async function createOrganisationFromName(name: string) { - const orgModel = new OrganisationModel(); - orgModel.name = name; - // console.log(orgModel); - const orgDoc = orgModel.save(); - return orgDoc; -} - -export function createOrganisation(org: Organisation) { - return OrganisationModel.create(org); -} - -// module.exports = CropService; diff --git a/backend/src/services/recommendations.service.ts b/backend/src/services/recommendations.service.ts index 379aa4069..03f9c550d 100644 --- a/backend/src/services/recommendations.service.ts +++ b/backend/src/services/recommendations.service.ts @@ -3,8 +3,7 @@ // const client = CloudantV1.newInstance({}); // const { plantedCrops, cropProductionForecast} = require("../db/cloudant"); -import LandAreasService from "./land-areas.service"; -import CropService from "./crop.service"; +import { cropService } from "./CropService"; // const nswBbox = "140.965576,-37.614231,154.687500,-28.071980"; // lng lat // const nswBboxLatLng = "-37.614231,140.965576,-28.071980,154.687500"; // lat lng @@ -22,17 +21,9 @@ export interface RecommendationsRequest { } export default class RecommendationsService { - lotAreaService: LandAreasService; - cropService: CropService; - - constructor() { - this.lotAreaService = new LandAreasService(); - this.cropService = new CropService(); - } - async getRecommendations(request: RecommendationsRequest) { this.createOrUpdateShortlistForLot(request); - const cropDetails = await this.cropService.getAllCrops(); + const cropDetails = await cropService.getAllCrops(); const plantDate = new Date(request.plantDate); const plantMonth = plantDate.getMonth() + 1; @@ -68,41 +59,41 @@ export default class RecommendationsService { crops[crop.toLowerCase()].shortlist = 100; }); - const overallCropDistribution: any = await this.lotAreaService.getOverallCropDistribution(); - const minArea = Math.min(...overallCropDistribution.map(dist => dist.area)); - - overallCropDistribution.forEach((dist) => { - if (crops[dist.crop.toLowerCase()]) { - crops[dist.crop.toLowerCase()].area = dist.area; - } - }); - - const cropProductionForecast: any = await this.lotAreaService.getCropProductionForecast(); - cropProductionForecast.forEach((dist) => { - const harvestDate = new Date(dist.date); - const crop = crops[dist.crop.toLowerCase()]; - if (harvestDate <= crop.harvestEnd && harvestDate >= crop.harvestStart) { - crop.yield += dist.yield; - } - }); - const minYield = Math.min(...cropDetails.map(crop => crops[crop.name.toLowerCase()].yield)); + // const overallCropDistribution: any = await this.lotAreaService.getOverallCropDistribution(); + // const minArea = Math.min(...overallCropDistribution.map(dist => dist.area)); + // + // overallCropDistribution.forEach((dist) => { + // if (crops[dist.crop.toLowerCase()]) { + // crops[dist.crop.toLowerCase()].area = dist.area; + // } + // }); + // + // const cropProductionForecast: any = await this.lotAreaService.getCropProductionForecast(); + // cropProductionForecast.forEach((dist) => { + // const harvestDate = new Date(dist.date); + // const crop = crops[dist.crop.toLowerCase()]; + // if (harvestDate <= crop.harvestEnd && harvestDate >= crop.harvestStart) { + // crop.yield += dist.yield; + // } + // }); + // const minYield = Math.min(...cropDetails.map(crop => crops[crop.name.toLowerCase()].yield)); const cropScores: any = []; - cropDetails.forEach((cropDetail) => { - const crop = crops[cropDetail.name.toLowerCase()]; - const cropScore: any = {}; - cropScore.crop = cropDetail.name; - cropScore.shortlistScore = crop.shortlist / 100 * weights.onShortlist; - cropScore.inSeasonScore = crop.inSeason / 100 * weights.inSeason; - cropScore.plantedAreaScore = crop.area === 0 ? 0 : (minArea / crop.area * weights.lowPlantedArea); - cropScore.yieldForecastScore = crop.yield === 0 ? 0 : (minYield / crop.yield * weights.lowYieldForecast); - cropScore.score = 10 * (cropScore.shortlistScore + - cropScore.inSeasonScore + - cropScore.plantedAreaScore + - cropScore.yieldForecastScore); - cropScores.push(cropScore); - }); + // cropDetails.forEach((cropDetail) => { + // const crop = crops[cropDetail.name.toLowerCase()]; + // const cropScore: any = {}; + // cropScore.crop = cropDetail.name; + // cropScore.shortlistScore = crop.shortlist / 100 * weights.onShortlist; + // cropScore.inSeasonScore = crop.inSeason / 100 * weights.inSeason; + // cropScore.plantedAreaScore = crop.area === 0 ? 0 : (minArea / crop.area * weights.lowPlantedArea); + // cropScore.yieldForecastScore = crop.yield === 0 ? 0 : (minYield / crop.yield * weights.lowYieldForecast); + // cropScore.score = 10 * (cropScore.shortlistScore + + // cropScore.inSeasonScore + + // cropScore.plantedAreaScore + + // cropScore.yieldForecastScore); + // cropScores.push(cropScore); + // }); return cropScores.sort((a, b) => b.score - a.score); } diff --git a/backend/src/sockets/socket.io.ts b/backend/src/sockets/socket.io.ts index c221c15d2..04b66e504 100644 --- a/backend/src/sockets/socket.io.ts +++ b/backend/src/sockets/socket.io.ts @@ -1,7 +1,7 @@ -import { MessageLog } from "./../db/entities/messageLog"; +import { MessageLog } from "../db/entities/messageLog"; import { Server as NodejsServer } from "http"; import { Namespace, Server } from "socket.io"; -import { Organisation } from "./../db/entities/organisation"; +import { Organisation } from "common-types"; // interface ServerToClientEvents { @@ -67,7 +67,7 @@ export class SocketIOManager { getNamespaceOfOrg(org: Organisation) { this.ensureInitialised() - return this.ioServer.of(`/org-${org._id}`); + return this.ioServer.of(`/org-${org.name}`); } publishMessage(org: Organisation, message: MessageLog) { @@ -77,7 +77,7 @@ export class SocketIOManager { publish(org: Organisation, event: string, ...args: any) { this.ensureInitialised() - console.log("[Socket IO Server] Publishing Event. Namespace:", `/org-${org._id}`, "Event:", event, "Args", ...args); + console.log("[Socket IO Server] Publishing Event. Namespace:", `/org-${org.name}`, "Event:", event, "Args", ...args); const nsp = this.getNamespaceOfOrg(org); nsp.emit(event, ...args); diff --git a/backend/src/types.d.ts b/backend/src/types.d.ts index 5bbcea49c..12f024eea 100644 --- a/backend/src/types.d.ts +++ b/backend/src/types.d.ts @@ -1,5 +1,3 @@ -import * as express from "express" - declare global { namespace Express { interface Request { @@ -7,4 +5,4 @@ declare global { session: any } } -} \ No newline at end of file +} diff --git a/common-types/data-model/Farm.ts b/common-types/data-model/Farm.ts new file mode 100644 index 000000000..a186a90d8 --- /dev/null +++ b/common-types/data-model/Farm.ts @@ -0,0 +1,18 @@ +import Farmer from "./Farmer"; +import { Polygon } from "geojson"; +import Field, { NewField } from "./Field"; + +export interface NewFarm { + _id?: string, + name: string; + farmer: Farmer; + fields: NewField[]; + geoShape?: Polygon; +} + +export default interface Farm extends NewFarm { + _id: string, + fields: Field[] +} + + diff --git a/common-types/data-model/Field.ts b/common-types/data-model/Field.ts new file mode 100644 index 000000000..2dc4b6cec --- /dev/null +++ b/common-types/data-model/Field.ts @@ -0,0 +1,15 @@ +import { Polygon } from "geojson"; +import FieldCrop, { NewFieldCrop } from "./FieldCrop"; + + +export interface NewField { + _id?: string, + name: string; + geoShape: Polygon; + crops: NewFieldCrop[] +} + +export default interface Field extends NewField { + _id: string, + crops: FieldCrop[] +} diff --git a/common-types/data-model/FieldCrop.ts b/common-types/data-model/FieldCrop.ts new file mode 100644 index 000000000..ebd17ad39 --- /dev/null +++ b/common-types/data-model/FieldCrop.ts @@ -0,0 +1,7 @@ +import Crop from "./Crop"; + +export default interface FieldCrop { + crop: Crop, + planted_date: Date, + harvested_date?: Date +} diff --git a/common-types/data-model/coopManager.ts b/common-types/data-model/User.ts similarity index 59% rename from common-types/data-model/coopManager.ts rename to common-types/data-model/User.ts index 5824cfca4..30d2d3a06 100644 --- a/common-types/data-model/coopManager.ts +++ b/common-types/data-model/User.ts @@ -1,5 +1,6 @@ +import { Point } from "geojson"; -export interface CoopManager { +export default interface User { /** * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" */ @@ -7,7 +8,8 @@ export interface CoopManager { /** * GeoCode / LatLng coordinate tuple */ - location: number[], - mobile: string, - coopOrganisations: string[] + location: Point, + mobile: string } + + diff --git a/common-types/data-model/crop.ts b/common-types/data-model/crop.ts index f83f9d559..9bb3894b8 100644 --- a/common-types/data-model/crop.ts +++ b/common-types/data-model/crop.ts @@ -1,8 +1,9 @@ -export interface Crop { +export default interface Crop { _id?: string, type: string, name: string, - planting_season: Date[], + planting_season: number[], time_to_harvest: number, + yield_per_sqm: number, is_ongoing: boolean } diff --git a/common-types/data-model/farmer.ts b/common-types/data-model/farmer.ts index d411c96f2..f415406d9 100644 --- a/common-types/data-model/farmer.ts +++ b/common-types/data-model/farmer.ts @@ -1,23 +1,15 @@ +import Farm, { NewFarm } from "./Farm"; - -export interface Farmer { - _id?: Types.ObjectId, +export interface NewFarmer { + _id?: string, name: string, mobile: string[], - coopOrganisations: string[] - land_ids: string[] - lands?: Land[] + address: string, + organisation: string, + farms: NewFarm[] } -export const FarmerSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, - name: String, - mobile: [String], - coopOrganisations: [String], - land_ids: [ObjectId] -}); - -export const FarmerModel = model("farmer", FarmerSchema); \ No newline at end of file +export default interface Farmer extends NewFarmer{ + _id: string, + farms: Farm[] +} diff --git a/common-types/data-model/land.ts b/common-types/data-model/land.ts deleted file mode 100644 index b2a1e2474..000000000 --- a/common-types/data-model/land.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Using Node.js `require()` -import { Schema, Model, model, Types } from 'mongoose'; -import { CropSchema, Crop } from "./crop"; -import { FarmerSchema, Farmer } from './farmer'; - -const ObjectId = Schema.Types.ObjectId; - -export interface FarmerCrop { - _id?: Types.ObjectId, - farmer: Farmer, - crop: Crop -} - -export const FarmerCropSchema = new Schema({ - _id: ObjectId, - farmer: FarmerSchema, - crop: CropSchema -}); - -export interface Land { - _id?: Types.ObjectId, - type: string, - fid: number, - name: string, - crops: FarmerCrop[] -} - -export const LandSchema = new Schema({ - _id: ObjectId, - type: String, - fid: Number, - name: String, - crops: [FarmerCropSchema] -}); - -export const LandModel = model("land", LandSchema); \ No newline at end of file diff --git a/common-types/data-model/organisation.ts b/common-types/data-model/organisation.ts index 0ec005eb2..f07a48021 100644 --- a/common-types/data-model/organisation.ts +++ b/common-types/data-model/organisation.ts @@ -1,20 +1,11 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Organisation { - _id?: Types.ObjectId, - name: string +import User from "./User"; +import { AuthMethod } from "../globals"; +import { EISConfig, WeatherCompanyConfig } from "../types"; + +export default interface Organisation { + name: string, + authMethod: AuthMethod, + users: User[], + eisConfig?: EISConfig, + weatherCompanyConfig?: WeatherCompanyConfig } - -export const OrganisationSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, - name: String, -}); - -export const OrganisationModel = model("organisation", OrganisationSchema); diff --git a/common-types/dto/OrganisationDto.ts b/common-types/dto/OrganisationDto.ts new file mode 100644 index 000000000..d01b902aa --- /dev/null +++ b/common-types/dto/OrganisationDto.ts @@ -0,0 +1,8 @@ +import { UserDto } from "./UserDto"; +import { AuthMethod } from "../globals"; + +export default interface OrganisationDto { + name: string; + authMethod: AuthMethod; + users: Omit[] +} diff --git a/common-types/dto/UserDto.ts b/common-types/dto/UserDto.ts new file mode 100644 index 000000000..9b09d3307 --- /dev/null +++ b/common-types/dto/UserDto.ts @@ -0,0 +1,14 @@ +import { Point } from "geojson"; + +export type UserDto = { + location: Point; + id: string; + email: string; + organisation: string, + mobile: string +} + +export type NewUserDto = UserDto & { + id?: string +} + diff --git a/backend/src/integrations/weather-company-api.types.ts b/common-types/globals.ts similarity index 88% rename from backend/src/integrations/weather-company-api.types.ts rename to common-types/globals.ts index 61aea2c65..a35341e78 100644 --- a/backend/src/integrations/weather-company-api.types.ts +++ b/common-types/globals.ts @@ -1,4 +1,4 @@ -export enum Languages { +export enum Language { Amharic_Ethiopia = "am-ET", Arabic_United_Arab_Emirates = "ar-AE", Azerbaijani_Azerbaijan = "az-AZ", @@ -92,7 +92,7 @@ export enum Languages { /** * Measurement units to return from the API */ -export enum Units { +export enum Unit { imperial = "e", metric = "m", /** @@ -101,28 +101,23 @@ export enum Units { SI = "s" } -/** - * LatLng representation for the Weather Company - */ -export interface GeoCode { - latitude: number; - longitude: number; -} - /** * Result format of the API Call * NOTE: Not every api call supports CSV. */ -export enum Formats { +export enum DataFormat { JSON = "json", CSV = "csv" } -export interface CommonOptions { - format: Formats; - language: Languages; - units: Units -} +export const isDefined = (value: T | null | undefined): value is T => { + return value !== undefined && value != null; +}; -export type LatLng = GeoCode; +export const isUndefined = (value: T | null | undefined): value is undefined | null => { + return value === undefined || value == null; +}; +export enum AuthMethod { + IBM_ID = "IBM_ID" +} diff --git a/common-types/integrations/EISConfig.ts b/common-types/integrations/EISConfig.ts new file mode 100644 index 000000000..f236e132f --- /dev/null +++ b/common-types/integrations/EISConfig.ts @@ -0,0 +1,6 @@ +export type EISConfig = { + apiUrl: string; + tokenUrl: string; + apiKey: string; + clientId: string; +} diff --git a/common-types/integrations/WeatherCompanyConfig.ts b/common-types/integrations/WeatherCompanyConfig.ts new file mode 100644 index 000000000..f276c4d29 --- /dev/null +++ b/common-types/integrations/WeatherCompanyConfig.ts @@ -0,0 +1,7 @@ +import { CommonOptions } from "../types"; + +export type WeatherCompanyConfig = { + apiUrl: string; + apiKey: string; + options?: CommonOptions +} diff --git a/common-types/package.json b/common-types/package.json new file mode 100644 index 000000000..32e92b4c2 --- /dev/null +++ b/common-types/package.json @@ -0,0 +1,232 @@ +{ + "name": "common-types", + "version": "1.0.0", + "types": "types.d.ts", + "dependencies": { + "@types/geojson": "^7946.0.8" + }, + "eslintConfig": { + "overrides": [ + { + "files": [ + "**/*.ts?(x)" + ], + "extends": [ + "plugin:react/recommended" + ], + "plugins": [ + "eslint-plugin-prefer-arrow", + "eslint-plugin-react", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "warn", + "@typescript-eslint/array-type": [ + "warn", + { + "default": "array" + } + ], + "@typescript-eslint/ban-types": [ + "warn", + { + "types": { + "Object": { + "message": "Avoid using the `Object` type. Did you mean `object`?" + }, + "Function": { + "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." + }, + "Boolean": { + "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" + }, + "Number": { + "message": "Avoid using the `Number` type. Did you mean `number`?" + }, + "String": { + "message": "Avoid using the `String` type. Did you mean `string`?" + }, + "Symbol": { + "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" + } + } + } + ], + "@typescript-eslint/consistent-type-assertions": "warn", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-member-accessibility": [ + "off", + { + "accessibility": "explicit" + } + ], + "@typescript-eslint/indent": "warn", + "@typescript-eslint/member-delimiter-style": [ + "warn", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ], + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": [ + "warn", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-shadow": [ + "warn", + { + "hoist": "all" + } + ], + "@typescript-eslint/no-unused-expressions": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/quotes": [ + "warn", + "double" + ], + "@typescript-eslint/semi": [ + "warn", + "always" + ], + "@typescript-eslint/triple-slash-reference": [ + "warn", + { + "path": "always", + "types": "prefer-import", + "lib": "always" + } + ], + "@typescript-eslint/type-annotation-spacing": "warn", + "@typescript-eslint/unified-signatures": "warn", + "brace-style": [ + "warn", + "1tbs" + ], + "complexity": "off", + "constructor-super": "warn", + "curly": "warn", + "eol-last": "warn", + "eqeqeq": [ + "warn", + "smart" + ], + "guard-for-in": "warn", + "id-blacklist": [ + "warn", + "any", + "Number", + "number", + "String", + "string", + "Boolean", + "boolean", + "Undefined", + "undefined" + ], + "id-match": "warn", + "indent": "off", + "max-classes-per-file": [ + "warn", + 1 + ], + "max-len": [ + "warn", + { + "code": 200 + } + ], + "new-parens": "warn", + "no-bitwise": "warn", + "no-caller": "warn", + "no-cond-assign": "warn", + "no-console": [ + "warn", + { + "allow": [ + "log", + "warn", + "dir", + "timeLog", + "assert", + "clear", + "count", + "countReset", + "group", + "groupEnd", + "table", + "dirxml", + "error", + "groupCollapsed", + "Console", + "profile", + "profileEnd", + "timeStamp", + "context" + ] + } + ], + "no-debugger": "warn", + "no-empty": "off", + "no-eval": "warn", + "no-fallthrough": "warn", + "no-invalid-this": "off", + "no-new-wrappers": "warn", + "no-redeclare": "warn", + "no-restricted-imports": "warn", + "no-throw-literal": "warn", + "no-trailing-spaces": "warn", + "no-undef-init": "warn", + "no-underscore-dangle": "off", + "no-unsafe-finally": "warn", + "no-unused-labels": "warn", + "no-var": "warn", + "object-shorthand": "warn", + "one-var": [ + "warn", + "never" + ], + "prefer-arrow/prefer-arrow-functions": "warn", + "prefer-const": "warn", + "radix": "warn", + "react/jsx-boolean-value": "warn", + "react/jsx-key": "warn", + "react/jsx-no-bind": "warn", + "react/self-closing-comp": "warn", + "spaced-comment": [ + "warn", + "always", + { + "markers": [ + "/" + ] + } + ], + "use-isnan": "warn", + "valid-typeof": "off", + "react/display-name": "warn" + } + } + ] + } +} diff --git a/common-types/types.d.ts b/common-types/types.d.ts new file mode 100644 index 000000000..a4357a73a --- /dev/null +++ b/common-types/types.d.ts @@ -0,0 +1,85 @@ +/** + * LatLng representation for the Weather Company + */ +import { DataFormat, isDefined, isUndefined, Language, Unit } from "./globals"; +import Crop from "./data-model/Crop"; +import Farm, { NewFarm } from "./data-model/Farm"; +import User, { OrganisationUser } from "./data-model/User"; +import Farmer, { NewFarmer } from "./data-model/Farmer"; +import Organisation from "./data-model/Organisation"; +import FieldCrop from "./data-model/FieldCrop"; +import Field, { NewField } from "./data-model/Field"; +import { NewUserDto, UserDto } from "./dto/UserDto"; +import OrganisationDto from "./dto/OrganisationDto"; +import { EISConfig } from "./integrations/EISConfig"; +import { WeatherCompanyConfig } from "./integrations/WeatherCompanyConfig"; +import { Point } from "geojson"; + +export { DataFormat, Language, Unit, isDefined, isUndefined }; + +export { Crop, Farm, NewFarm, User, OrganisationUser, Farmer, NewFarmer, Organisation, Field, NewField, FieldCrop }; + +export {EISConfig, WeatherCompanyConfig} + +export {UserDto, NewUserDto, OrganisationDto}; + +export type LatLngNumber = { + lat: number, + lng: number +} + +export type LatLngString = { + lat: string, + lng: string +} + +export type GeoCodeNumber = { + latitude: number, + longitude: number +} + +export type GeoCodeString = { + latitude: string, + longitude: string +} + +export type LatLng = LatLngNumber | LatLngString; +export type GeoCode = GeoCodeNumber | GeoCodeString; + +export function toLatLng(geoCode: GeoCode): LatLng { + return { + lat: geoCode.latitude, + lng: geoCode.longitude + } +} + +export function toGeoCode(latLng: LatLng): GeoCode { + return { + latitude: latLng.lat, + longitude: latLng.lng + } +} + +export function toGroCodeFromPoint(point: Point): GeoCodeNumber { + return { + latitude: point.coordinates[0], + longitude: point.coordinates[1] + } +} + +export function geoCodeToString(geocode: GeoCodeNumber) { + return `${geocode.latitude},${geocode.longitude}` +} + +export type BoundingBox = { + lowerLeft: LatLng, + upperRight: LatLng +} + +export interface CommonOptions { + format: DataFormat; + language: Language; + units: Unit +} + + diff --git a/docker-compose.yml b/docker-compose.yml index 37be3e9d8..56a6000c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,15 @@ services: - "./backend/.env.dev.production:/home/node/app/.env:ro" ports: - "3000:3000" + + mongodb: + container_name: 'mongodb-open_harvest' + image: mongo + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + ports: + - 27017:27017 + volumes: + - ~/mongodb:/data/db diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx index c8e4198c7..4c1c8660e 100644 --- a/react-app/src/App.tsx +++ b/react-app/src/App.tsx @@ -3,39 +3,35 @@ import "carbon-addons-iot-react/scss/styles.scss"; import PrivateRoute from "./helpers/privateRoute"; import "./App.scss"; -import { Content, Header, HeaderContainer, HeaderMenuItem, HeaderName, HeaderNavigation, SkipToContent } from "carbon-components-react"; +import { Content, HeaderContainer } from "carbon-components-react"; import { Redirect, Route, Switch, withRouter } from "react-router"; import { RouteComponentProps } from "react-router/ts4.0"; -import Nav from "./components/Nav/Nav" +import Nav from "./components/Nav/Nav"; import CoOpHome from "./components/CoOpHome/CoOpHome"; import Farmers from "./components/Farmers/Farmers"; import Crops from "./components/Crops/Crops"; -import { AuthContext, AuthProvider } from "./services/auth"; -import UserOnboarding from "./components/Onboarding/UserOnboarding"; +import { AuthProvider } from "./services/auth"; +import UserOnBoarding from "./components/Onboarding/UserOnboarding"; import { AddFarmer } from "./components/Farmers/AddFarmer"; -import {enableAllPlugins} from "immer" +import { enableAllPlugins } from "immer"; import { Messaging } from "./components/Messaging/Messaging"; + enableAllPlugins(); type AppProps = RouteComponentProps ; type AppState = { // showOnBoardingWizard: boolean; - showLogoutModal: boolean; + showLogoutModal?: boolean; + newUser?: boolean; }; class App extends Component { constructor(props: any) { super(props); - console.log(props.location); - this.state = { - // showOnBoardingWizard: false, - showLogoutModal: false - }; + this.state = {}; this.setShowLogoutModal = this.setShowLogoutModal.bind(this); - - } async componentDidMount() { @@ -45,15 +41,12 @@ class App extends Component { const res = await fetch("/api/coopManager/hasBeenOnBoarded"); const result = await res.json(); newUser = result.exists; - } - catch (e) {} - + } catch (e) {} + + this.setState({newUser}); + if (newUser) { - // this.state = { - // // showOnBoardingWizard: true, - // showLogoutModal: false - // }; - this.props.history.push('/onboarding') + this.props.history.push("/onboarding"); } } @@ -63,72 +56,88 @@ class App extends Component { <> ( + render={() => (