├── .firebaserc ├── architecture-diagram.png ├── smart-expenses-screens.png ├── services ├── invoke-workflow │ ├── package.json │ └── index.js ├── process-annotations │ ├── package.json │ └── index.js └── approval-callback │ ├── package.json │ └── index.js ├── firebase.json ├── public ├── js │ ├── main.js │ ├── common.js │ ├── admin.js │ └── request.js ├── css │ └── style.css ├── 404.html ├── index.html ├── request.html ├── _firebase.html └── admin.html ├── .gitignore ├── README.md ├── workflow.yaml ├── LICENSE └── architecture-diagram.drawio /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "easy-ai-serverless" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/smart-expenses/HEAD/architecture-diagram.png -------------------------------------------------------------------------------- /smart-expenses-screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/smart-expenses/HEAD/smart-expenses-screens.png -------------------------------------------------------------------------------- /services/invoke-workflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "start-batch-processing-workflow", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@google-cloud/workflows": "^1.2.5", 6 | "cors": "^2.8.5" 7 | } 8 | } -------------------------------------------------------------------------------- /services/process-annotations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "process-docai-annotations", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@google-cloud/storage": "^5.14.2", 6 | "@google-cloud/firestore": "^4.15.1" 7 | } 8 | } -------------------------------------------------------------------------------- /services/approval-callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approval-callback-call", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "cors": "^2.8.5", 6 | "node-fetch": "^2.6.1", 7 | "google-auth-library": "^7.1.1" 8 | } 9 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "site": "easy-ai-serverless", 4 | "public": "public", 5 | "ignore": [ 6 | "firebase.json", 7 | "**/.*", 8 | "**/node_modules/**" 9 | ], 10 | "rewrites": [ 11 | { 12 | "source": "/request/**", 13 | "destination": "/request.html" 14 | }, 15 | { 16 | "source": "/admin", 17 | "destination": "/admin.html" 18 | }, 19 | { 20 | "source": "/admin/**", 21 | "destination": "/admin.html" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | new Vue({ 16 | el: '#homepageButtons', 17 | data: { 18 | reportUrl: `/request/${new Hashids().encode(new Date().getTime() - 1627897194822)}`, 19 | } 20 | }) -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: var(--sl-font-sans); 3 | } 4 | 5 | body { 6 | padding: var(--sl-spacing-large); 7 | } 8 | 9 | h1 { 10 | background-color: rgb(var(--sl-color-primary-500)); 11 | color: rgb(var(--sl-color-neutral-0)); 12 | padding: var(--sl-spacing-large); 13 | border-radius: var(--sl-border-radius-x-large); 14 | } 15 | 16 | .thumbnail { 17 | object-fit: cover; 18 | object-position: center; 19 | height: 200px; 20 | width: 200px; 21 | margin: var(--sl-spacing-medium); 22 | box-shadow: var(--sl-shadow-large); 23 | border-radius: var(--sl-border-radius-x-large); 24 | border-width: var(--sl-spacing-x-small); 25 | border-color: white; 26 | } 27 | 28 | sl-tab::part(base), sl-breadcrumb-item::part(base) { 29 | font-size: var(--sl-font-size-x-large); 30 | } 31 | 32 | sl-tab-panel { 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: space-between; 36 | } 37 | 38 | sl-card { 39 | margin: 10px; 40 | box-shadow: var(--sl-shadow-x-large); 41 | } 42 | 43 | sl-details { 44 | box-shadow: var(--sl-shadow-large); 45 | } 46 | 47 | sl-details::part(header) { 48 | font-weight: var(--sl-font-weight-bold); 49 | } 50 | 51 | .card-header sl-icon-button { 52 | font-size: var(--sl-font-size-medium); 53 | } -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /services/approval-callback/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const cors = require('cors')({origin: true}); 16 | const fetch = require('node-fetch'); 17 | 18 | exports.approvalCallbackCall = async (req, res) => { 19 | cors(req, res, async () => { 20 | res.set('Access-Control-Allow-Origin', '*'); 21 | 22 | const {url, approved} = req.body; 23 | console.log("Approved? ", approved); 24 | console.log("URL = ", url); 25 | 26 | const {GoogleAuth} = require('google-auth-library'); 27 | const auth = new GoogleAuth(); 28 | const token = await auth.getAccessToken(); 29 | console.log("Token", token); 30 | 31 | try { 32 | const resp = await fetch(url, { 33 | method: 'POST', 34 | headers: { 35 | 'accept': 'application/json', 36 | 'content-type': 'application/json', 37 | 'authorization': `Bearer ${token}` 38 | }, 39 | body: JSON.stringify({ approved }) 40 | }); 41 | console.log("Response = ", JSON.stringify(resp)); 42 | 43 | const result = await resp.json(); 44 | console.log("Outcome = ", JSON.stringify(result)); 45 | 46 | res.status(200).json({status: 'OK'}); 47 | } catch(e) { 48 | console.error(e); 49 | 50 | res.status(200).json({status: 'error'}); 51 | } 52 | }); 53 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smart Expenses — New expense report 7 | 11 | 12 | 13 | 14 | 15 | 16 | 33 | 34 | 35 | 36 |
37 |
38 | 39 | 40 | New report 41 | 42 | 43 | 44 | 45 | Administration 46 | 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/js/common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Configure Firebase 16 | if (firebase !== undefined) { 17 | var firebaseConfig = { 18 | apiKey: "AIzaSyCPLllU6CIdfymjQvdEqPAgReB0Rn3Bm_A", 19 | authDomain: "easy-ai-serverless.firebaseapp.com", 20 | projectId: "easy-ai-serverless", 21 | storageBucket: "smart-expenses-incoming-receipts", 22 | messagingSenderId: "770605692057", 23 | appId: "1:770605692057:web:e7fa3f0b887fc8c5413408" 24 | }; 25 | // Initialize Firebase 26 | firebase.initializeApp(firebaseConfig); 27 | } 28 | 29 | // Let Vue.js ignore "sl-*" elements from Shoelace.js 30 | Vue.config.ignoredElements = [/^sl-/]; 31 | 32 | // Install a two-way binding directive for Shoelace components with Vue.js 33 | // Imported from: https://www.npmjs.com/package/@shoelace-style/vue-sl-model 34 | const wm = new WeakMap(); 35 | Vue.use({ 36 | install: function (Vue) { 37 | Vue.directive('sl-model', { 38 | bind (el, binding, vnode) { 39 | const inputHandler = event => Vue.set(vnode.context, binding.expression, event.target.value); 40 | wm.set(el, inputHandler); 41 | el.value = binding.value; 42 | el.addEventListener('input', inputHandler); 43 | }, 44 | componentUpdated(el, binding) { 45 | el.value = binding.value; 46 | }, 47 | unbind(el) { 48 | const inputHandler = wm.get(el); 49 | el.removeEventListener(el, inputHandler); 50 | } 51 | }) 52 | } 53 | }); -------------------------------------------------------------------------------- /services/invoke-workflow/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const cors = require('cors')({origin: true}); 16 | const {ExecutionsClient} = require('@google-cloud/workflows'); 17 | const client = new ExecutionsClient(); 18 | 19 | exports.invokeWorkflow = async (req, res) => { 20 | cors(req, res, async () => { 21 | res.set('Access-Control-Allow-Origin', '*'); 22 | 23 | const body = req.body; 24 | console.log("Incoming input: ", body); 25 | 26 | const PROJECT_ID = process.env.PROJECT_ID; 27 | const WORKFLOW_REGION = process.env.WORKFLOW_REGION; 28 | const WORKFLOW_NAME = process.env.WORKFLOW_NAME; 29 | console.log(`Project ID: ${PROJECT_ID} / Region: ${WORKFLOW_REGION} / Workflow: ${WORKFLOW_NAME}`); 30 | 31 | const argument = JSON.stringify(body); 32 | console.log("Stringified arg:", argument); 33 | 34 | try { 35 | const execResponse = await client.createExecution({ 36 | parent: client.workflowPath(PROJECT_ID, WORKFLOW_REGION, WORKFLOW_NAME), 37 | execution: { argument } 38 | }); 39 | console.log(`Batch processing receipts workflow execution request: ${JSON.stringify(execResponse)}`); 40 | 41 | const execName = execResponse[0].name; 42 | console.log(`Batch processing receipts workflow execution: ${execName}`); 43 | res.status(200).json({executionId: execName}); 44 | } catch (e) { 45 | console.error(e); 46 | res.status(500).json({error: e.message}); 47 | } 48 | }); 49 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # VSCode 107 | .vscode 108 | 109 | # Firebase 110 | .firebase -------------------------------------------------------------------------------- /public/js/admin.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | Vue.use(Vuefire.firestorePlugin, { wait: true }) 16 | 17 | const db = firebase.firestore(); 18 | 19 | var app = new Vue({ 20 | el: '#admin', 21 | data: { 22 | reportsAwaiting: [], 23 | reportsApproved: [], 24 | reportsRejected: [], 25 | reportsError: [], 26 | actionOnReport: [] 27 | }, 28 | firestore: { 29 | reportsAwaiting: db.collection("requests").where("status", "==", "AWAITING"), 30 | reportsApproved: db.collection("requests").where("status", "==", "APPROVED"), 31 | reportsRejected: db.collection("requests").where("status", "==", "REJECTED"), 32 | reportsError: db.collection("requests").where("status", "==", "ERROR"), 33 | }, 34 | methods: { 35 | approval: function(reportId, approval) { 36 | console.log(`${approval ? 'Approve' : 'Reject'} ${reportId}`); 37 | this.actionOnReport.push(reportId); 38 | this.callbackCall(reportId, approval); 39 | }, 40 | callbackCall: async function(reportId, approval) { 41 | const report = this.reportsAwaiting.find(report => report.id == reportId); 42 | 43 | // TODO: avoid hard-coding the function URL 44 | const callbackFunctionUrl = "https://europe-west1-easy-ai-serverless.cloudfunctions.net/approval-callback"; 45 | try { 46 | const callbackResp = await fetch(callbackFunctionUrl, { 47 | method: "POST", 48 | headers: { "Content-Type" : "application/json" }, 49 | body: JSON.stringify({ 50 | url: report.callback, 51 | approved: approval 52 | }) 53 | }); 54 | const outcome = await callbackResp.json(); 55 | console.log("Callback outcome", outcome); 56 | } catch (e) { 57 | // TODO: notify web UI in case of error 58 | console.error(e); 59 | } 60 | } 61 | } 62 | }) -------------------------------------------------------------------------------- /public/request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smart Expenses — New expense report 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

Expense report: {{reportId}}

22 | 23 | 24 | 25 | 26 | Home 27 | 28 | 29 | New expense report 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 | 44 |
45 | 46 | 47 | Select receipts 48 | 49 | 50 |
51 |
52 | 53 | 54 | {{ title }}
55 | {{ description }} 56 |
57 | 58 |
59 | 60 | 61 | 62 | Submit report 63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /services/process-annotations/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const {Storage} = require('@google-cloud/storage'); 16 | const storage = new Storage(); 17 | 18 | const Firestore = require('@google-cloud/firestore'); 19 | 20 | exports.processAnnotations = async (req, res) => { 21 | const {report_id, bucket_path} = req.body; 22 | 23 | console.log("Report", report_id); 24 | console.log("Bucket", bucket_path); 25 | 26 | const query = { 27 | prefix: report_id, 28 | }; 29 | const [files] = await storage.bucket(bucket_path).getFiles(query); 30 | 31 | const allSummaries = []; 32 | 33 | for await (const fileInfo of files) { 34 | var summary = new Map(); 35 | 36 | console.log("Analysing", fileInfo.name); 37 | const [file] = await fileInfo.download(); 38 | const {entities} = JSON.parse(file.toString()); 39 | 40 | console.log("Entities", JSON.stringify(entities)); 41 | 42 | var errorMsg = ""; 43 | try { 44 | const supplier = entities.find(e => e.type == 'supplier_name')?.normalizedValue?.text; 45 | if (!!supplier) summary.set('supplier', supplier); 46 | console.log("Supplier", supplier); 47 | } catch(e) { console.error(e); errorMsg += e.message + "\n"; } 48 | 49 | try { 50 | const lineItems = entities.filter(e => e.type == 'line_item').map(li => li?.normalizedValue?.text).filter(li => !!li); 51 | summary.set('lineItems', lineItems); 52 | } catch(e) { console.error(e); errorMsg += e.message + "\n"; } 53 | 54 | try { 55 | const total = entities.find(e => e.type == 'total_amount')?.normalizedValue?.text; 56 | if (!!total) summary.set('total', total); 57 | } catch(e) { console.error(e); errorMsg += e.message + "\n"; } 58 | 59 | try { 60 | const currency = entities.find(e => e.type == 'currency')?.normalizedValue?.text; 61 | if (!!currency) summary.set('currency', currency); 62 | } catch(e) { console.error(e); errorMsg += e.message + "\n"; } 63 | 64 | const summaryObject = Object.fromEntries(summary); 65 | console.log(`SUMMARY (${fileInfo.name})`, summaryObject); 66 | allSummaries.push({file: fileInfo.name, summary: summaryObject}); 67 | }; 68 | 69 | const requestStore = new Firestore().collection('requests'); 70 | const doc = requestStore.doc(report_id); 71 | await doc.set({ 72 | summary: allSummaries 73 | }, {merge: true}); 74 | 75 | if (errorMsg.length > 0) { 76 | res.send({ 77 | status: 'Error', 78 | summary: allSummaries, 79 | message: errorMsg 80 | }); 81 | } else { 82 | res.send({ 83 | status: 'OK', 84 | summary: allSummaries 85 | }); 86 | } 87 | }; -------------------------------------------------------------------------------- /public/_firebase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 40 | 41 | 42 |
43 |

Welcome

44 |

Firebase Hosting Setup Complete

45 |

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

46 | Open Hosting Documentation 47 |
48 |

Firebase SDK Loading…

49 | 50 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /public/js/request.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const allStates = { 16 | NOT_SUBMITTED: ['primary', 'info-circle', 'Report not submitted', 'The report has not been submitted yet.'], 17 | UPLOADED: ['primary', 'info-circle', 'Receipts uploaded', 'Your receipts have been uploaded successfully.'], 18 | SUBMITTED: ['primary', 'check2-circle', 'Report submitted', 'Your report has been submitted, and will be processed soon, then your manager will review your submission.'], 19 | PROCESSING: ['primary', 'info-circle', 'Report is being processed', 'Your report has been submitted, and is currently being processed.'], 20 | AWAITING: ['primary', 'info-circle', 'Report is awaiting validation', 'Your manager has received your report, will review it, and then validate or reject it.'], 21 | APPROVED: ['success', 'check2-circle', 'Report validated', 'Your manager has validated your report.'], 22 | REJECTED: ['danger', 'exclamation-octagon', 'Report rejected', 'Your manager has rejected your report.'], 23 | ERROR: ['danger', 'exclamation-octagon', 'Error occured', 'An error occurred while processing your report. Please contact your IT department.'] 24 | } 25 | 26 | const reportId = window.location.pathname.substring(9); 27 | 28 | var app = new Vue({ 29 | el: '#request', 30 | data: { 31 | reportId: reportId, 32 | images: [], 33 | status: "NOT_SUBMITTED", 34 | alert: allStates["NOT_SUBMITTED"][0], 35 | icon: allStates["NOT_SUBMITTED"][1], 36 | title: allStates["NOT_SUBMITTED"][2], 37 | description: allStates["NOT_SUBMITTED"][3] 38 | }, 39 | watch: { 40 | status: function(newVal, oldVal) { 41 | this.alert = allStates[this.status][0]; 42 | this.icon = allStates[this.status][1]; 43 | this.title = allStates[this.status][2]; 44 | this.description = allStates[this.status][3]; 45 | } 46 | }, 47 | methods: { 48 | onFileChange: function(e) { 49 | var files = e.target.files || e.dataTransfer.files; 50 | if (!files.length) return; 51 | 52 | this.images = []; 53 | var vm = this; 54 | Array.from(files).forEach(f => { 55 | var reader = new FileReader(); 56 | reader.onload = (e) => { 57 | vm.images.push({ 58 | src: e.target.result, 59 | title: f.name 60 | }); 61 | }; 62 | reader.readAsDataURL(f); 63 | }); 64 | }, 65 | clickFileChooser: function(e) { 66 | document.querySelector("#filesInput").click(e); 67 | } 68 | } 69 | }) 70 | 71 | const db = firebase.firestore(); 72 | db.collection("requests").doc(reportId).onSnapshot((doc) => { 73 | if (doc.data()) { 74 | app.status = doc.data().status; 75 | console.log("New status from Firestore: ", app.status); 76 | } 77 | }); 78 | 79 | document.querySelector("#formFiles").addEventListener('sl-submit', async (event) => { 80 | const reportId = app.reportId; 81 | const fileList = Array.from(document.querySelector("#filesInput").files); 82 | 83 | 84 | // store in GCS via Firestore Storage 85 | const storageRef = firebase.storage().ref(); 86 | for await (const f of fileList) { 87 | const receiptRef = storageRef.child(`${reportId}/${f.name}`); 88 | const uploadSnapshot = await receiptRef.put(f); 89 | const downloadUrl = await uploadSnapshot.ref.getDownloadURL(); 90 | console.log("Uploaded", f.name); 91 | console.log("Downlaod URL", downloadUrl); 92 | } 93 | 94 | app.status = "UPLOADED"; 95 | 96 | // call function to start workflow execution 97 | // TODO: avoid hard-coded function URL 98 | const fnUrl = "https://europe-west1-easy-ai-serverless.cloudfunctions.net/invoke-workflow"; 99 | try { 100 | const fnWorkflowResp = await fetch(fnUrl, { 101 | method: "POST", 102 | headers: { "Content-Type" : "application/json" }, 103 | body: JSON.stringify({ reportId }) 104 | }); 105 | const outcome = await fnWorkflowResp.json(); 106 | console.log("Workflow execution", outcome); 107 | } catch (e) { 108 | // TODO: notify web UI in case of error 109 | console.error(e); 110 | } 111 | 112 | app.status = "SUBMITTED"; 113 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Expenses sample demonstration 2 | 3 | Here is the architecture diagram of the application demonstrated 4 | in the Cloud Next 2021 [talk](https://cloud.withgoogle.com/next/catalog?session=DEV202#application-development) 5 | **"AI-powered applications with Google Cloud"** (DEV202). 6 | 7 | > Learn how you can harness the power of serverless computing with AI services like Document AI. 8 | > Build applications with speed, ease, and intelligence. See how AI-powered use cases, 9 | > combined with Workflows and Cloud Functions, can help automate use cases at your enterprise. 10 | > We’ll show you how to leverage pretrained models for form types like including receipts, 11 | > invoice processing, and more. Developers will be able to increase productivity 12 | > by spending less time worrying about infrastructure, and dedicate more time 13 | > building applications for their businesses. 14 | 15 | ![Architecture diagram](architecture-diagram.png) 16 | 17 | This demo application is a smart expense report application, consisting of two main screens: 18 | 19 | * a page for employees where they can select receipts and submit their expense reports, 20 | * a page for manager to review, then approve or reject submitted expense reports. 21 | 22 | ![Employee and manager expense report screens](smart-expenses-screens.png) 23 | 24 | The project makes use of the following [Google Cloud](https://cloud.google.com/storage) products: 25 | 26 | * [Document AI](https://cloud.google.com/document-ai) to parse and analyse receipts to find meaningful information (supplier, line items, amounts, currency), 27 | * [Workflows](https://cloud.google.com/workflows) to organize the overall business process, invoking Document AI and data analysis function, to update status in Firestore, to create a callback waiting for the manager's approval, 28 | * [Cloud Functions](https://cloud.google.com/functions) to make the link between frontend and backend, and to extract data from Document AI's JSON output, 29 | * [Firestore](https://cloud.google.com/firestore) to store all the data about the expense reports and receipts 30 | * [Cloud Stroage](https://cloud.google.com/storage) to store pictures of receipts, 31 | * [Firebase](https://firebase.google.com/) on the frontend to upload receipt pictures and to keep UI up-to-date with the realtime status of the expense report processing. 32 | 33 | The project is using: 34 | 35 | * [Vue.js](https://vuejs.org/) as its frontend progressive JavaScript framework, 36 | * [Shoelace](https://shoelace.style/) as its library of web components, 37 | * [Node.js](https://nodejs.org/en/) for the code of the few cloud functions. 38 | 39 | # Some project setup notes 40 | 41 | ## Deploying the sample within your own project 42 | 43 | Certain URLs pointing at cloud storage buckets or function locations are hard-coded. 44 | To deploy the sample application in your own Google Cloud Project, you should update: 45 | 46 | In `workflow.yaml`: 47 | * The `bucket_input` and `bucket_output` variables to point at globally unique GCS buckets. 48 | * In `invoke_document_ai` step, point at your location and at the right Document AI processor ID. 49 | * In `process_annotations` step, point at the URL of your cloud function analysing Document AI's JSON output. 50 | 51 | In `public/js/common.js`: 52 | * Update the Firebase project configuration to point at your project (see Firebase [documentation](https://firebase.google.com/docs/web/learn-more#config-object)) 53 | 54 | In `public/js/admin.js`: 55 | * Change the `callbackFunctionUrl` URL to point at the URL of your approval callback function. 56 | 57 | In `public/js/request.js`: 58 | * Change the `fnUrl` URL to point at the URL of your workflow invocation function. 59 | 60 | ## Deploying the workflow definition 61 | 62 | ``` 63 | gcloud workflows deploy batch-process-receipts \ 64 | --location=europe-west4 \ 65 | --source=workflow.yaml 66 | ``` 67 | 68 | ## Web assets with Firebase 69 | 70 | Login with Firebase (use `--no-localhost` flag if running in Cloud Shell): 71 | ``` 72 | firebase login 73 | ``` 74 | 75 | Deploy static assets to Firebase hosting 76 | ``` 77 | firebase deploy --only hosting 78 | ``` 79 | 80 | Run local server: 81 | ``` 82 | firebase serve 83 | ``` 84 | 85 | ## Functions deployment 86 | 87 | Environment variables 88 | ``` 89 | export FUNCTION_REGION=europe-west1 90 | export WORKFLOW_REGION=europe-west4 91 | export WORKFLOW_NAME=batch-process-receipts 92 | ``` 93 | 94 | Function invoking the workflow from the web frontend: 95 | ``` 96 | gcloud functions deploy invoke-workflow \ 97 | --region=${FUNCTION_REGION} \ 98 | --source=./services/invoke-workflow \ 99 | --runtime nodejs14 \ 100 | --entry-point=invokeWorkflow \ 101 | --set-env-vars PROJECT_ID=${GOOGLE_CLOUD_PROJECT},WORKFLOW_REGION=${WORKFLOW_REGION},WORKFLOW_NAME=${WORKFLOW_NAME} \ 102 | --trigger-http \ 103 | --allow-unauthenticated 104 | ``` 105 | 106 | Function calling the workflow callback from the web frontend: 107 | ``` 108 | gcloud functions deploy approval-callback \ 109 | --region=${FUNCTION_REGION} \ 110 | --source=./services/approval-callback \ 111 | --runtime nodejs14 \ 112 | --entry-point=approvalCallbackCall \ 113 | --trigger-http \ 114 | --allow-unauthenticated 115 | ``` 116 | 117 | *Note:* The service account used by the function calling the callback URL should have the `Workflows Editor` (or `Workflows Admin`) and `Service Account Token Creator` permissions. 118 | 119 | Function analysing the Doc AI output 120 | ``` 121 | gcloud functions deploy process-annotations \ 122 | --region=${FUNCTION_REGION} \ 123 | --source=./services/process-annotations \ 124 | --runtime nodejs14 \ 125 | --entry-point=processAnnotations \ 126 | --trigger-http 127 | ``` 128 | 129 | ## Firestore setup 130 | 131 | To allow the web pages to access the data (in read-only mode) in Firestore, the security rules for Firestore should be updated with: 132 | 133 | ``` 134 | rules_version = '2'; 135 | service cloud.firestore { 136 | match /databases/{database}/documents { 137 | match /{document=**} { 138 | allow read, write: if false; 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | # License 145 | 146 | All solutions within this repository are provided under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Please see the [LICENSE](https://github.com/GoogleCloudPlatform/smart-expenses/blob/main/LICENSE) file for more detailed terms and conditions. 147 | 148 | # Disclaimer 149 | 150 | This repository and its contents are not an official Google Product. -------------------------------------------------------------------------------- /workflow.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | main: 16 | params: [input] 17 | steps: 18 | - start: 19 | call: sys.log 20 | args: 21 | text: ${input} 22 | - vars: 23 | assign: 24 | - report_id: ${input.reportId} 25 | - bucket_input: "gs://smart-expenses-incoming-receipts/" 26 | - bucket_output: "gs://smart-expenses-parsed-receipts/" 27 | - project: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} 28 | - update_status_to_submitted: 29 | call: update_firestore 30 | args: 31 | report_id: ${report_id} 32 | field: "status" 33 | value: "PROCESSING" 34 | 35 | - invoke_document_ai: 36 | call: googleapis.documentai.v1.projects.locations.processors.batchProcess 37 | args: 38 | name: ${"projects/" + project + "/locations/eu/processors/c799b15b13e7c838"} 39 | location: "eu" 40 | body: 41 | inputDocuments: 42 | gcsPrefix: 43 | gcsUriPrefix: ${bucket_input + report_id} 44 | documentOutputConfig: 45 | gcsOutputConfig: 46 | gcsUri: ${bucket_output + report_id} 47 | skipHumanReview: true 48 | result: document_ai_response 49 | - log_document_ai_response: 50 | call: sys.log 51 | args: 52 | text: ${document_ai_response} 53 | 54 | - check_error_or_response: 55 | switch: 56 | - condition: ${"error" in document_ai_response} 57 | next: on_error 58 | next: process_annotations 59 | - on_error: 60 | call: update_firestore 61 | args: 62 | report_id: ${report_id} 63 | field: "status" 64 | value: "ERROR" 65 | - return_error: 66 | return: ${document_ai_response.error} 67 | 68 | - process_annotations: 69 | call: http.post 70 | args: 71 | url: https://europe-west1-easy-ai-serverless.cloudfunctions.net/process-annotations 72 | auth: 73 | type: OIDC 74 | body: 75 | report_id: ${report_id} 76 | bucket_path: ${bucket_output} 77 | result: process_annotations_result 78 | - log_process_annotations_result: 79 | call: sys.log 80 | args: 81 | text: ${process_annotations_result} 82 | 83 | - create_callback: 84 | call: events.create_callback_endpoint 85 | args: 86 | http_callback_method: "POST" 87 | result: callback_details 88 | - log_callback_details: 89 | call: sys.log 90 | args: 91 | text: ${callback_details} 92 | 93 | - store_callback_details: 94 | call: update_firestore 95 | args: 96 | report_id: ${report_id} 97 | field: "callback" 98 | value: ${callback_details.url} 99 | - update_status_to_awaiting_approval: 100 | call: update_firestore 101 | args: 102 | report_id: ${report_id} 103 | field: "status" 104 | value: "AWAITING" 105 | - await_callback: 106 | try: 107 | call: events.await_callback 108 | args: 109 | callback: ${callback_details} 110 | timeout: 3600 111 | result: callback_request 112 | except: 113 | as: e 114 | steps: 115 | - update_status_to_error: 116 | call: update_firestore 117 | args: 118 | report_id: ${report_id} 119 | field: "status" 120 | value: "ERROR" 121 | - log_error: 122 | call: sys.log 123 | args: 124 | severity: "ERROR" 125 | text: ${"Awaiting callback error " + e.message} 126 | next: return_final_step 127 | - assign_approval: 128 | assign: 129 | - approved: ${callback_request.http_request.body.approved} 130 | 131 | - store_approval: 132 | switch: 133 | - condition: ${callback_request.http_request.body.approved} 134 | steps: 135 | - store_approved: 136 | call: update_firestore 137 | args: 138 | report_id: ${report_id} 139 | field: "status" 140 | value: "APPROVED" 141 | - condition: ${not callback_request.http_request.body.approved} 142 | steps: 143 | - store_rejected: 144 | call: update_firestore 145 | args: 146 | report_id: ${report_id} 147 | field: "status" 148 | value: "REJECTED" 149 | - return_final_step: 150 | return: ${document_ai_response.metadata} 151 | 152 | update_firestore: 153 | params: [report_id, field, value, type: "string"] 154 | steps: 155 | - define_db_root: 156 | assign: 157 | - database_root: ${"projects/" + sys.get_env("GOOGLE_CLOUD_PROJECT_ID") + "/databases/(default)/documents/requests/"} 158 | - firestoreType: ${type + "Value"} 159 | - fieldTypeAndValue: {} 160 | - fieldTypeAndValue[firestoreType]: ${value} 161 | - body: {fields: {}} 162 | - body.fields[field]: ${fieldTypeAndValue} 163 | - log_firestore_change_request: 164 | call: sys.log 165 | args: 166 | text: ${report_id + " > " + field + " = " + json.encode_to_string(value) + " (" + type + ")"} 167 | - patch_doc_firestore: 168 | call: googleapis.firestore.v1.projects.databases.documents.patch 169 | args: 170 | name: ${database_root + report_id} 171 | updateMask: 172 | fieldPaths: [${field}] 173 | body: ${body} 174 | result: firestore_update_result 175 | - log_firestore_change_response: 176 | call: sys.log 177 | args: 178 | text: ${firestore_update_result} 179 | -------------------------------------------------------------------------------- /public/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smart Expenses — Administration 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

Expense reports

23 | 24 | 25 | 26 | 27 | Home 28 | 29 | 30 | Manager approval 31 | 32 | 33 |
34 | 35 | 36 | 37 |   38 | Awaiting approval 39 | 40 | 41 |   42 | Approved 43 | 44 | 45 |   46 | Rejected 47 | 48 | 49 |   50 | Error 51 | 52 | 53 | 54 | 86 | 87 | 88 | 112 | 113 | 114 | 138 | 139 | 140 | 164 | 165 | 166 | 167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /architecture-diagram.drawio: -------------------------------------------------------------------------------- 1 | 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 --------------------------------------------------------------------------------