<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Oin Corner</title><description>The online workshop of Ahmad Tohir, a software engineer and product builder working on operational software, products, and technical experiments.</description><link>https://www.oincorner.my.id</link><item><title>Modeling a Logistics Workflow with F#</title><link>https://www.oincorner.my.id/notes/fsharp-logistic-workflow</link><guid isPermaLink="true">https://www.oincorner.my.id/notes/fsharp-logistic-workflow</guid><description>Exploring discriminated unions and pattern matching to model a logistics order workflow in F#.</description><pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When an order moves through a logistics system, its status is more than a label. It is a set of business rules: an unassigned order can receive a driver, an assigned order can start, and a completed order must not move again.&lt;/p&gt;
&lt;p&gt;This small &lt;a href=&quot;https://github.com/oinpentuls/fsharp-logistic-workflow&quot;&gt;F# Logistic Workflow repository&lt;/a&gt; is my way of learning F# domain modeling through that concrete problem. It models an order workflow with discriminated unions (DUs), pattern matching, and &lt;code&gt;Result&lt;/code&gt; values.&lt;/p&gt;
&lt;h2&gt;The workflow&lt;/h2&gt;
&lt;p&gt;The happy path is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Unassigned → Assigned → Started → Arrived → Completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An order can also fail or be cancelled. &lt;code&gt;Completed&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;, and &lt;code&gt;Cancelled&lt;/code&gt; are terminal states: any command received after that should return an &lt;code&gt;InvalidTransition&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;Commands express an intent, and some carry data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Command =
    | Assign of DriverId: string
    | Start
    | Arrive
    | Fail of Reason: string
    | Complete
    | Cancel of Reason: string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using commands rather than setting a status directly keeps the transition rules in one place. It also makes failure and cancellation reasons part of the domain rather than incidental strings attached later.&lt;/p&gt;
&lt;h2&gt;First pass: one flat union&lt;/h2&gt;
&lt;p&gt;The simplest version represents every status in one DU and matches each valid status-command pair:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type OrderStatus =
    | Unassigned
    | Assigned
    | Started
    | Arrived
    | Failed
    | Completed
    | Cancelled

let executeCommand currentStatus command : Result&amp;lt;OrderStatus, DomainError&amp;gt; =
    match currentStatus, command with
    | Unassigned, Assign _ -&amp;gt; Ok Assigned
    | Assigned, Start -&amp;gt; Ok Started
    | Started, Arrive -&amp;gt; Ok Arrived
    | Arrived, Complete -&amp;gt; Ok Completed
    | Completed, _ | Failed, _ | Cancelled, _ -&amp;gt;
        Error (InvalidTransition (currentStatus, command))
    | _, Cancel _ -&amp;gt; Ok Cancelled
    | _, Fail _ -&amp;gt; Ok Failed
    | _ -&amp;gt; Error (InvalidTransition (currentStatus, command))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is already a useful improvement over scattered &lt;code&gt;if&lt;/code&gt; statements or a large mutable state machine. All legal transitions are visible, and everything else becomes an explicit domain error. But the type still treats active and terminal states as peers, so that distinction is enforced mostly by the match expression.&lt;/p&gt;
&lt;h2&gt;Second pass: group active and terminal states&lt;/h2&gt;
&lt;p&gt;The next version makes the lifecycle visible in the type shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ActiveStatus =
    | Unassigned
    | Assigned
    | Started
    | Arrived

type TerminalStatus =
    | Failed of Reason: string
    | Cancelled of Reason: string
    | Completed

type OrderStatus =
    | Active of ActiveStatus
    | Terminal of TerminalStatus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the rule for completed work is compact and hard to miss:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Terminal _, _ -&amp;gt; Error (InvalidTransition (currentStatus, command))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The compiler helps us maintain the model by checking the DU cases we handle. Pattern matching still performs the runtime decision for a particular command, but the model makes terminal states an explicit category instead of a convention we have to remember.&lt;/p&gt;
&lt;h2&gt;Third pass: model the initial state separately&lt;/h2&gt;
&lt;p&gt;The final experiment separates &lt;code&gt;Unassigned&lt;/code&gt; from work that is already in progress:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type CancellableStatus = Unassigned
type ActiveStatus = Assigned | Started | Arrived

type OrderStatus =
    | Cancellable of CancellableStatus
    | Active of ActiveStatus
    | Terminal of TerminalStatus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes an important business distinction clearer: only an unassigned order can receive a driver. The transition starts from &lt;code&gt;Cancellable Unassigned&lt;/code&gt; and moves to &lt;code&gt;Active Assigned&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Cancellable Unassigned, Assign _ -&amp;gt; Ok (Active Assigned)
| Active Assigned, Start -&amp;gt; Ok (Active Started)
| Active Started, Arrive -&amp;gt; Ok (Active Arrived)
| Active Arrived, Complete -&amp;gt; Ok (Terminal Completed)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the direction I find most interesting. Good types cannot replace every validation rule, but they can make impossible or confusing states harder to express and make the business language visible in the code.&lt;/p&gt;
&lt;h2&gt;Running a workflow&lt;/h2&gt;
&lt;p&gt;Each version folds a list of commands over a starting status. The fold stops naturally on the first &lt;code&gt;Error&lt;/code&gt;, while successful runs keep a history of states for display.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let commands = [ Assign &quot;Driver1&quot;; Start; Arrive; Complete ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That sequence produces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Unassigned → Assigned → Started → Arrived → Completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The project also includes failure, cancellation, and invalid-transition examples. Run them with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;F# does not make workflow design automatic, but DUs and pattern matching give the rules a concise, inspectable home. Instead of spreading status checks through application code, we can describe the domain states, the commands that change them, and the errors that explain rejected transitions.&lt;/p&gt;
&lt;p&gt;For a real logistics system, I would add richer identifiers, persistence, timestamps, authorization, and tests. As a focused learning project, though, this progression from a flat union to grouped states is a clear demonstration of how types can guide domain design.&lt;/p&gt;
</content:encoded><author>Ahmad Tohir</author></item><item><title>Gap Based Sequence</title><link>https://www.oincorner.my.id/notes/gap-based-sequence</link><guid isPermaLink="true">https://www.oincorner.my.id/notes/gap-based-sequence</guid><description>Understanding Gap Based Sequences for frequent re-ordering cases</description><pubDate>Wed, 19 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br /&gt;
Use gap-based sequences for frequent re-ordering&lt;br /&gt;
It will optimize your update queries&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;I&apos;ve been given a task to build an API that supports re-ordering in Planner Menu.
The planner works like Trello cards, where you can freely move items around.&lt;/p&gt;
&lt;h2&gt;First Attempt&lt;/h2&gt;
&lt;p&gt;When I heard &quot;re-ordering&quot; in a collection of tasks/orders, the first thing that came to my mind was a
simple sequential order like 1, 2, 3... and so on.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;task id&lt;/th&gt;
&lt;th&gt;sequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;task-1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The idea is simple: assign sequential numbers and voila, the task are ordered.
Now, let&apos;s say we want to move &lt;code&gt;task-5&lt;/code&gt; to the position of &lt;code&gt;task-2&lt;/code&gt;. The result would be:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;task id&lt;/th&gt;
&lt;th&gt;sequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;task-1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-5&lt;/td&gt;
&lt;td&gt;2 &amp;lt;- task-5 gets sequence 2 and the rest are re-synced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The re-ordering works!
But now let&apos;s see it from the database perspective.&lt;/p&gt;
&lt;p&gt;When we update the sequence of &lt;code&gt;task-5&lt;/code&gt; from 5 to 2, we also need to update the affected tasks.
So we will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query the task to check if it exists&lt;/li&gt;
&lt;li&gt;Update its sequence&lt;/li&gt;
&lt;li&gt;Update every affected task&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the total update queries for one operation is &lt;strong&gt;5&lt;/strong&gt; for &lt;strong&gt;5 rows&lt;/strong&gt;.
This seems fine at first, but now imagine 100 tasks. That would becomes &lt;strong&gt;100 update queries!&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And since re-ordering will happen frequently, we need a better approach.&lt;/p&gt;
&lt;h2&gt;Research &amp;amp; Exploration&lt;/h2&gt;
&lt;p&gt;I needed a way to handle frequent ordering more efficiently.
How can I minimize the number of updates in the database for one drag and drop operation?&lt;/p&gt;
&lt;p&gt;Luckily, I found a question on StackExchange about &lt;a href=&quot;https://softwareengineering.stackexchange.com/questions/195308/storing-a-re-orderable-list-in-a-database&quot;&gt;ordering&lt;/a&gt;.
Gap-based sequencing seems more suitable in my case because it can handle frequent re-ordering with far fewer update queries. (Usually only &lt;strong&gt;one&lt;/strong&gt; row is affected ).&lt;/p&gt;
&lt;p&gt;If the gap is becomes to small, we can rebalance the sequence.
This does update all tasks, but only happens when the gaps are exhausted.&lt;/p&gt;
&lt;h2&gt;Second Attempt&lt;/h2&gt;
&lt;p&gt;I&apos;m using default gap value of &lt;code&gt;100&lt;/code&gt;, so the table looks like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;task id&lt;/th&gt;
&lt;th&gt;sequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;task-1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-2&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-3&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-4&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-5&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Now, if we want to move the &lt;code&gt;task-5&lt;/code&gt; to the position between &lt;code&gt;task-1&lt;/code&gt; and &lt;code&gt;task-2&lt;/code&gt;, we need the previous and next sequences around the target:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Previous sequence is &lt;code&gt;task-1&lt;/code&gt; with sequence of &lt;strong&gt;100&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Next sequence is &lt;code&gt;task-2&lt;/code&gt; with sequence of &lt;strong&gt;200&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we can calculate the new sequence for our &lt;code&gt;task-5&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int previous = 100;
int next = 200;

int gap = next - previous; // 100
int newSequence = previous + (gap /2) // 100 + (100 /2) = 150
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;task id&lt;/th&gt;
&lt;th&gt;sequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;task-1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-5&lt;/td&gt;
&lt;td&gt;150 &amp;lt;-- midpoint of 200/100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-2&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-3&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-4&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On the database side, we only need to update &lt;strong&gt;1 row&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Handle Edge Case&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;how to handle if I move the items to the beginning?&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int? previous = null;
int next = 100;
int newSequence = next / 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;how to handle if I move the items to the end?&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const DEFAULT_GAP_VALUE = 100;

int previous = 500;
int? next = null;

int newSequence = previous + DEFAULT_GAP_VALUE;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;how to rebalance if the gap become too small?&lt;br /&gt;
We can set the sequence of the collections to its initial sequence using default sequence value
This is a snippet from my implementation of the &lt;code&gt;GapSequencer&lt;/code&gt; class.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class GapSequencer
{
    public const int DEFAULT_GAP_SIZE = 100;
    private readonly int _gapSize;
    private int _currentSequence = 0;

    public GapSequencer() : this(DEFAULT_GAP_SIZE)
    {
    }

    public GapSequencer(int gapSize)
    {
        _gapSize = gapSize;
    }

    public int GetSequence()
    {
        _currentSequence += _gapSize;
        return _currentSequence;
    }

    public void Reset()
    {
        _currentSequence = 0;
    }

    public void InitializeSequences&amp;lt;T&amp;gt;(IEnumerable&amp;lt;T&amp;gt; items, Action&amp;lt;T, int&amp;gt; setSequence)
    {
        Reset();
        foreach (var item in items)
        {
            setSequence(item, GetSequence());
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// Calculates a new sequence number to insert between &amp;lt;paramref name=&quot;previousSequence&quot;/&amp;gt; and &amp;lt;paramref name=&quot;nextSequence&quot;/&amp;gt;.
    /// At least one of the two must be provided. The result maintains order and avoids collisions.
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;previousSequence&quot;&amp;gt;The sequence number before the insertion point (nullable).&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;nextSequence&quot;&amp;gt;The sequence number after the insertion point (nullable).&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;A new integer sequence value strictly between previous and next (if both provided).&amp;lt;/returns&amp;gt;
    /// &amp;lt;exception cref=&quot;ArgumentException&quot;&amp;gt;
    /// Thrown when both inputs are null or when previousSequence &amp;gt;= nextSequence.
    /// &amp;lt;/exception&amp;gt;
    /// &amp;lt;exception cref=&quot;InvalidOperationException&quot;&amp;gt;
    /// Thrown when there&apos;s insufficient gap to insert a value without collision.
    /// &amp;lt;/exception&amp;gt;
    public static int CalculateNewSequence(int? previousSequence, int? nextSequence)
    {
        if (!previousSequence.HasValue &amp;amp;&amp;amp; !nextSequence.HasValue)
        {
            throw new ArgumentException(&quot;At least one of previousSequence or nextSequence must be set&quot;);
        }

        if (!previousSequence.HasValue &amp;amp;&amp;amp; nextSequence.HasValue)
        {
            if (nextSequence.Value &amp;lt;= 1)
            {
                throw new InvalidOperationException(&quot;Gap between sequences is too small. Consider rebalancing the gap size.&quot;);
            }

            return nextSequence.Value / 2;
        }

        if (!nextSequence.HasValue &amp;amp;&amp;amp; previousSequence.HasValue)
        {
            return previousSequence.Value + DEFAULT_GAP_SIZE;
        }

        int prev = previousSequence!.Value;
        int next = nextSequence!.Value;

        if (prev &amp;gt;= next)
        {
            throw new ArgumentException($&quot;previousSequence ({prev}) must be less than nextSequence ({next})&quot;);
        }

        int gap = next - prev;
        
        if (gap &amp;lt;= 1)
        {
            throw new InvalidOperationException(&quot;Gap between sequences is too small. Consider rebalancing the gap size.&quot;);
        }

        return prev + (gap / 2);
    }

    public static void RebalanceSequences&amp;lt;T&amp;gt;(IEnumerable&amp;lt;T&amp;gt; items, Action&amp;lt;T, int&amp;gt; setSequence)
    {
        var sequencer = new GapSequencer();
        sequencer.InitializeSequences(items, setSequence);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;When working on a feature, we often have a solution in mind right away.
We know the requirement, we know what to do, so let&apos;s start!&lt;/p&gt;
&lt;p&gt;But sometimes we later realize there&apos;s much better approach.&lt;/p&gt;
&lt;p&gt;Sequential ordering is simple and works,&lt;br /&gt;
but if the order is changing frequently, a &lt;strong&gt;gap based sequence&lt;/strong&gt; is far more efficient.&lt;/p&gt;
</content:encoded><author>Ahmad Tohir</author></item><item><title>.NET Data Protection API</title><link>https://www.oincorner.my.id/notes/data-protection-api</link><guid isPermaLink="true">https://www.oincorner.my.id/notes/data-protection-api</guid><description>Protect your data using built-in package by .NET</description><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Data Protection API&lt;/h2&gt;
&lt;p&gt;Before we start, I want to share a bit of background. The Data Protection API is commonly used in Windows,
but thankfully, the ASP.NET team has provided us with a cross-platform solution through the
&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-9.0&quot;&gt;ASP.NET Data Protection API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This topic is quite broad and will likely span across multiple posts.
I&apos;ll do my best to simplify it without losing the key concepts.&lt;/p&gt;
&lt;p&gt;The Data Protection API helps us securely protect data and support compliance with various data protection regulations like &lt;a href=&quot;https://gdpr-info.eu/&quot;&gt;GDPR&lt;/a&gt;, &lt;a href=&quot;https://www.parl.ca/legisinfo/en/bill/44-1/c-27&quot;&gt;Digital Charter Implementation Act&lt;/a&gt;, &lt;a href=&quot;https://oag.ca.gov/privacy/ccpa&quot;&gt;CCPA&lt;/a&gt;, &lt;a href=&quot;https://www.pdpc.gov.sg/overview-of-pdpa/the-legislation/personal-data-protection-act&quot;&gt;PDPA&lt;/a&gt; and &lt;a href=&quot;https://peraturan.bpk.go.id/Details/229798/uu-no-27-tahun-2022&quot;&gt;Indonesia&apos;s PDP&lt;/a&gt;, among others.&lt;/p&gt;
&lt;p&gt;This is a crucial step for me because I&apos;m currently building an HRIS (Human Resource Information System) that stores employee data, admin data, and owner information. This also including sensitive data like performance reviews.&lt;/p&gt;
&lt;p&gt;In the upcoming posts, I&apos;ll likely start with the HRIS roadmap, followed by a discussion on data regulations and then dive into the web app.&lt;/p&gt;
</content:encoded><author>Ahmad Tohir</author></item></channel></rss>