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:
    1. Sends a POST request to https://storeganise.sensorberg.io/oauth/token .
    2. Uses client credentials (client ID, client secret) and a scope for authentication.
    3. Returns the access token from the response.

Rent

  • Purpose: Handles the completion of unit move-ins.
  • Steps:
    1. Check if the user exists, if not, create a new user.
    2. Creates a new group membership for the rental
    3. Generates a random PIN for the owner if not already set.
    4. Updates the access code and PIN in the Storeganise system.

Vacate

  • Purpose: Handles the completion of unit move-outs.
  • Steps:
    1. Deletes the group membership associated with the rental, effectively revoking access.

Lockout

  • Purpose: Handles locking out a unit, and updating the overlocking status.
  • Steps:
    1. Updates the ends-at attribute of the user group membership to lock the unit.
    2. Updates the overlocking status in the Storeganise system.

SetAccessCode

  • Purpose: Sets the access code for a user.
  • Steps:
    1. Updates the identification code (access code) for the user in the Sensorberg system.
    2. 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:
    1. Verifies the necessary parameters (businessCode and API key).
    2. Fetches job and site details from the Storeganise API.
    3. Check if the security system is 'sensorberg' for the site; if not, ignore the site.
    4. Handles different Storeganise events (unit_moveIn , unit_moveOut , etc.) by invoking corresponding functions.
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(businessCode, `/v1/admin/users/${owner.id}`, { method: 'PUT', body: {customFields: {sensorberg_pin: owner.customFields.sensorberg_pin}} });

    await setAccessCode({owner});
  }

  await fetchSg(businessCode, `/v1/admin/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(businessCode, `/v1/admin/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(businessCode, `/v1/admin/users/${owner.id}`, { method: 'PUT', body: {customFields: {sensorberg_pin: pin}} });
  }

  const rentals = await fetchSg(businessCode, `/v1/admin/unit-rentals?ownerId=${owner.id}&state=occupied`);
  await Promise.all(
    rentals.map(r => fetchSg(businessCode, `/v1/admin/unit-rentals/${r.id}`, { method: 'PUT', body: {customFields: {sensorberg_pin: pin}} }))
  );
}



export default async (req, res) => {
  const { businessCode } = req.query || {};

  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${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(`/v1/admin/jobs/${req.body.data?.jobId}`).catch(() => null);
  const site = job && await fetchSg(`/v1/admin/sites/${job.result.siteId}?include=customFields`).catch(() => null);
  if (!site) return res.json({message: 'missing job/site'});
  if (site.customFields.securitySystem !== 'sensorberg') return res.json({message: 'ignore site'});

  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, unit, owner] = await Promise.all([
          fetchSg(`/v1/admin/unit-rentals/${job.result.unitRentalId}?include=customFields`),
          fetchSg(`/v1/admin/units/${job.result.unitId}?include=customFields`),
          fetchSg(`/v1/admin/users/${job.result.ownerId}?include=customFields`),
        ]);

        await rent({ unit, owner, rental, fetchSg, fetchSensorberg });
        break;
      }

      case 'job.unit_moveOut.completed': {
        const [rental, unit] = await Promise.all([
          fetchSg(`/v1/admin/unit-rentals/${job.result.unitRentalId}?include=customFields`),
          fetchSg(`/v1/admin/units/${job.result.unitId}?include=customFields`),
        ]);

        await vacate({ rental, fetchSensorberg });
        break;
      }

      case 'job.unit_transfer.completed': {
        const [oldRental, newRental, oldUnit, newUnit, owner] = await Promise.all([
          fetchSg(`/v1/admin/unit-rentals/${job.result.oldRentalId}?include=customFields`),
          fetchSg(`/v1/admin/unit-rentals/${job.result.newRentalId}?include=customFields`),
          fetchSg(`/v1/admin/units/${job.result.oldUnitId}?include=customFields`),
          fetchSg(`/v1/admin/units/${job.result.newUnitId}?include=customFields`),
          fetchSg(`/v1/admin/users/${job.result.ownerId}?include=customFields`),
        ]);
        await vacate({ rental: oldRental, fetchSensorberg });
        await rent({ unit: newUnit, owner, rental: newRental, fetchSg, fetchSensorberg });
        break;
      }
      
      case 'unitRental.updated': {
        const rental = await fetchSg(`/v1/admin/unit-rentals/${job.result.unitRentalId}?include=customFields`);
        const owner = await fetchSg(`/v1/admin/users/${rental.ownerId}?include=customFields`);
  
        if (req.body.data.changedKeys?.includes('customFields.sensorberg_pin')) {
          await setAccessCode({ owner, pin: rental.customFields.sensorberg_pin, fetchSg, fetchSensorberg });
        }

        if (req.body.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(`/v1/admin/unit-rentals?include=unit,customFields&ids=${job.result.unitRentalIds || job.result.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 });
  }
}

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us