Integration example case study - Sensorberg
Code Snippet - Case study with Sensorberg
Here's now a complete integration code with Sensorberg, for this we define several functions:
Get Access Token
- Purpose: Retrieves an access token from the Storeganise OAuth server.
- Steps:
- Sends a POST request to
https://storeganise.sensorberg.io/oauth/token
. - Uses client credentials (client ID, client secret) and a scope for authentication.
- Returns the access token from the response.
- Sends a POST request to
Rent
- Purpose: Handles the completion of unit move-ins.
- Steps:
- Check if the user exists, if not, create a new user.
- Creates a new group membership for the rental
- Generates a random PIN for the owner if not already set.
- Updates the access code and PIN in the Storeganise system.
Vacate
- Purpose: Handles the completion of unit move-outs.
- Steps:
- Deletes the group membership associated with the rental, effectively revoking access.
Lockout
- Purpose: Handles locking out a unit, and updating the overlocking status.
- Steps:
- Updates the ends-at attribute of the user group membership to lock the unit.
- Updates the overlocking status in the Storeganise system.
SetAccessCode
- Purpose: Sets the access code for a user.
- Steps:
- Updates the identification code (access code) for the user in the Sensorberg system.
- If the PIN has changed, update it in the Storeganise system for all occupied units.
Main Exported Function
- Purpose: Acts as a webhook handler for Storeganise events.
- Steps:
- Verifies the necessary parameters (
businessCode
and API key). - Fetches job and site details from the Storeganise API.
- Check if the security system is 'sensorberg' for the site; if not, ignore the site.
- Handles different Storeganise events (
unit_moveIn
,unit_moveOut
, etc.) by invoking corresponding functions.
- Verifies the necessary parameters (
async function getAccessToken() { const auth = await fetch('https://storeganise.sensorberg.io/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `grant_type=client_credentials&client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}&scope=app`, }).then(r => r.json()); return auth.access_token; } async function rent({ unit, owner, rental, fetchSg, fetchSensorberg }) { let user; try { user = await fetchSensorberg(`users/${owner.id}`); } catch (err) { if (err.status !== 404) throw err; user = await fetchSensorberg('users', { method: 'POST', data: { type: 'users', attributes: { 'full-name': owner.name, email: owner.email, 'external-identifier': owner.id, locale: owner.language, } } }).catch(err => { if (err.status !== 422) throw err; console.log('User already exists, skip', err.message); }); } const gm = await fetchSensorberg('user_group_memberships', { method: 'POST', data: { type: 'user-group-memberships', attributes: { 'external-identifier': rental.id, 'external-user-identifier': owner.id, 'external-user-group-identifier': unit.customFields.sensorberg_id, 'created-at': new Date().toJSON().replace(/\.\d*/, ''), 'starts-at': new Date(rental.startDate).toJSON().replace(/\.\d*/, ''), // 'ends-at': '2099-12-31T00:00:00Z' } } }); if (!owner.customFields.sensorberg_pin) { owner.customFields.sensorberg_pin = `${Math.floor(1_000_000 * Math.random())}`.padStart(4, 0); await fetchSg(`users/${owner.id}`, { method: 'PUT', body: { customFields: { sensorberg_pin: owner.customFields.sensorberg_pin } } }); await setAccessCode({ owner }); } await fetchSg(`unit-rentals/${rental.id}`, { method: 'PUT', body: { customFields: { sensorberg_pin: owner.customFields.sensorberg_pin } } }); } async function vacate({ rental, fetchSensorberg }) { return fetchSensorberg(`user_group_memberships/${rental.id}`, { method: 'DELETE' }); } async function lockOut({ rental, overlocked = rental.overdue, fetchSg, fetchSensorberg }) { await fetchSensorberg(`user_group_memberships/${rental.id}`, { method: 'PUT', data: { type: 'user-group-memberships', attributes: { 'ends-at': new Date(overlocked || '2099-12-31T23:59:59Z').toJSON().replace(/\.\d*/, ''), } } }); await fetchSg(`unit-rentals/${rental.id}`, { method: 'PUT', body: { customFields: { sensorberg_overlocked: !!overlocked } } }); } async function setAccessCode({ owner, pin = owner.customFields.sensorberg_pin, fetchSg, fetchSensorberg }) { await fetchSensorberg(`users/${owner.id}/identification_code`, { method: 'PUT', data: { type: 'users', attributes: { 'identification-code': pin } } }); if (pin !== owner.customFields.sensorberg_pin) { await fetchSg(`users/${owner.id}`, { method: 'PUT', body: { customFields: { sensorberg_pin: pin } } }); } const rentals = await fetchSg(`unit-rentals?ownerId=${owner.id}&state=occupied`); await Promise.all( rentals.map(r => fetchSg(`unit-rentals/${r.id}`, { method: 'PUT', body: { customFields: { sensorberg_pin: pin } } })) ); } export default async (req, res) => { const { businessCode } = req.query || {}; const { data } = req.body || {}; if (!businessCode) return res.json({ message: 'missing businessCode' }); if (!process.env[`API_KEY_${businessCode}`]) return res.json({ message: 'missing API_KEY for ${businessCode}' }); async function fetchSg(path, opts = {}) { return fetch(`https://${businessCode}.storeganise.com/api/v1/admin/${path}`, { method: opts.method, headers: { Authorization: `ApiKey ${process.env[`API_KEY_${businessCode}`]}`, ...opts.body && { 'Content-Type': 'application/json' }, }, body: opts.body && JSON.stringify(opts.body), }) .then(async r => { const data = await r.json().catch(() => ({})); if (r.ok) return data; const err = Object.assign(new Error(), data.error); err.status = r.status; throw err; }); } const job = await fetchSg(`jobs/${data.jobId}`).catch(() => null); const site = job && await fetchSg(`sites/${job.result.siteId}?include=customFields`).catch(() => null); if (!site) return res.json({ message: 'missing job/site' }); // When the middleware is used as an addon, use this instead: // const addon = await fetchSg(`addons/${data.addonId}`); // then use credentials stored in addon.customFields instead of site.customFields const accessToken = await getAccessToken(); async function fetchSensorberg(path, { method = 'GET', data } = {}) { return fetch(`https://storeganise.sensorberg.io/api/backend-sdk/v1/${path}`, { method, headers: { 'content-type': 'application/vnd.api+json', 'authorization': `Bearer ${accessToken}`, }, body: data && JSON.stringify({ data }), }) .then(async r => { return r.json().then(result => { if (!r.ok) { throw Object.assign(new Error(JSON.stringify(result.errors || result)), { status: r.status, method, path }); } return result.data; }); }); } try { switch (req.body.type) { case 'job.unit_moveIn.completed': { const rental = await fetchSg(`unit-rentals/${data.unitRentalId}?include=unit,owner,customFields`); await rent({ unit: rental.unit, owner: rental.owner, rental, fetchSg, fetchSensorberg }); break; } case 'job.unit_moveOut.completed': { const rental = await fetchSg(`unit-rentals/${data.unitRentalId}?include=customFields`); await vacate({ rental, fetchSensorberg }); break; } case 'job.unit_transfer.completed': { const oldRental = await fetchSg(`unit-rentals/${data.oldRentalId}?include=unit,customFields`); const newRental = await fetchSg(`unit-rentals/${data.newRentalId}?include=unit,owner,customFields`); await vacate({ rental: oldRental, fetchSensorberg }); await rent({ unit: newRental.unit, owner: newRental.owner, rental: newRental, fetchSg, fetchSensorberg }); break; } case 'unitRental.updated': { const rental = await fetchSg(`unit-rentals/${data.unitRentalId}?include=owner,customFields`); if (data.changedKeys?.includes('customFields.sensorberg_pin')) { await setAccessCode({ owner: rental.owner, pin: rental.customFields.sensorberg_pin, fetchSg, fetchSensorberg }); } if (data.changedKeys?.includes('customFields.sensorberg_overlocked')) { await lockOut({ rental, overlocked: rental.customFields.sensorberg_overlocked ? new Date() : null, fetchSg, fetchSensorberg }); } break; } case 'unitRental.markOverdue': case 'unitRental.unmarkOverdue': { await fetchSg(`unit-rentals?include=unit,customFields&ids=${data.unitRentalIds || data .unitRentalId}`) .then(rentals => Promise.all( rentals.map(rental => { return lockOut({ rental, fetchSg, fetchSensorberg }); }) )); break; } } res.json({ message: 'ok' }); } catch(err) { console.error(err); res.status(400).json({ message: err.message }); } }