DEVELOPER

Back to Developer Blog

technical

PAX SDK Payments Part 1: Building a Simple POS in C# with the .NET Framework

By Laura Olson | October 13th, 2024


Introduction

Businesses that need to accept in-person credit card payments from their custom Point of Sale (POS) applications are responsible for keeping their customers’ payment information secure and meeting lengthy Payment Card Industry (PCI) regulations—unless they use a Semi-Integrated payment solution.

With a Semi-Integrated (SI) solution, the credit card terminal communicates sensitive transaction information directly to the payment processor, keeping that data out of the business’s systems. This shifts most of the responsibility of PCI compliance away from your POS application and the merchant’s environment, and onto the payment provider.

In Part 1 of this article, you’ll learn how to build a simple POS app, and Part 2 will explain how to add credit card payment functionality to it using the C#/.NET PAX SI SDK.

Build this App

Clone this code repository to quickly create your own app today!

Prerequisites

  1. Review the North Developer Integration Guide and follow the instructions in the Setup section.
  2. Install Visual Studio Community or use your preferred IDE with support for C#/.NET projects. VS Community will be used for this tutorial.
  3. Create a new VS Community project. Select the “Windows Forms App (.NET Framework)” template. This tutorial assumes little or no prior knowledge, so using this template makes it quick and easy to create a simple POS UI for instructional purposes. Install the POSLink.dll provided in PAX’s POSLink .NET package.

Building a Simple Point of Sale Application

This tutorial will explain each step in detail, both in the article text and the code comments. Because the app in this tutorial is for educational purposes, the code may not be as minimized as possible so that all aspects of each step are clearly illustrated.

The POS will have two pages:

  1. Order Summaries: This is the default page that will be displayed when the application is started. It will display a list of all orders with high-level information about each record, including the order number, the employee assigned to the order, the table number, the order total, and the order status (eg., Open, Refunded, Voided, etc.). It will also provide buttons to create a new order and submit the current batch of orders to the processor for settlement and funding.
Point of Sale Application
  1. Order Details: This page will be displayed when the user clicks on an existing record in the Order Summaries list or the “Start New Order” button is clicked. The Order Details page will display information about the selected order or the new order, and provide buttons to add items to the order and perform a variety of payment functions.

The payment buttons will be created in this part of the tutorial, and payment functionality will be added in Part 2.

Point of Sale with PAX Payment Functionailty

Get in Touch

Talk to us about using the PAX SDK to accept in-person payments with your Point of Sale application.

Creating the Class Files

Form1

A file named Form1.cs should be included in your project by default. An instance of this class named OrderSummaries will be created when the application runs.

Form2

In the Solution Explorer, locate your project (this is the item with your project name that can be collapsed and expanded). Right click on your project, select Add, then Class. In the Add New Item window, rename the class “Form2.cs” (or any meaningful name of your choice), and click Add. An instance of this class named OrderDetails will be created when the application runs.

FormProvider

The FormProvider static class will be used to implement a version of the Singleton Design Pattern so that only one instance of each Form class can be created when the app is running, while providing a simple method of accessing that instance.

Add a new class file named “FormProvider.cs” (or any meaningful name of your choice) to the project, and add the following code to it.

public static class FormProvider
{
       public static Form1 OrderSummaries
        {
            get
            {
                if (orderSummaries == null)
                {
                    orderSummaries = new Form1();
                }
                return orderSummaries;
            }
        }
        private static Form1 orderSummaries;

        public static Form2 OrderDetail
        {
            get
            {
                if (orderDetail == null)
                {
                    orderDetail = new Form2();
                }
                return orderDetail;
            }
        }
        private static Form2 orderDetail;
}

The Form1 object will be named OrderSummaries, and the Form2 object will be named OrderDetail. In the Solution Explorer, open Program.cs and update the Application.Run method to the following:

Application.Run(FormProvider.OrderSummaries);

Setting Up a Flat-File Database

In real contexts, each business’s database will be setup differently, so this tutorial will use a CSV in place of a database for simplicity, with the expectation that in real-world scenarios, each business will connect their own secure database based on their specific business requirements.

In the Solution Explorer, locate your project, right click, select Add, then New Folder. Name the new folder App_Data. Right click on the App_Data folder, select Add New Item, then name the file orders.csv. Open orders.csv and insert the following in the first row.

Order,Employee,Table,Status,Total,Items,Subtotal,Tax,Tip,AuthId,TransactionId,BatchId,AuthToken,TransactionToken,AuthAmount

These are the column headers, or data attributes, for your flat-file database. They indicate the types of data that the app will be writing and reading for each record.

POS Page 1: Order Summaries

If the Design View of Form1.cs is displayed by default (tab name is “Form1.cs [Design]”), use the Solution Explorer to locate Form1.cs, right click, and select “View Code” to view the Form1 code.

At the beginning of the file, add the following Using statements to access the required .NET namespaces. If your environment is missing any, select Project from the top menu, then Manage NuGet Packages, search for the necessary package, and install.

using System;
using System.IO;
using System.Data;
using System.Linq;
using System.Windows.Forms;
using FileHelpers;
using System.Collections.Generic;
using Newtonsoft.Json;

Within the Form1 class definition that’s provided by default, add the code below to initialize class-level variables.

        //instantiate a public class-level data table to store the data for all orders.
        public DataTable ordersTable { get; set; }

        //instantiate a public class-level dictionary to store the data for each order’s items.
        public Dictionary<string, decimal> itemDictionary { get; set; }

        //instantiate a public class-level data view to access data from the ordersTable for a specific order.
        public DataView selectedOrder { get; set; }

        //initialize a public class-level string to track the payment authorization ID for each authorization in a batch.
        public string authId { get; set; } = "0";

        //initialize a public class-level string to track the payment transaction ID for each transaction in a batch.
        public string transactionId { get; set; } = "0";

        //initialize a public class-level string to track the batch ID for each batch of transactions.
        public string batchId { get; set; } = "0";

Accessing the Database

Within the Form1 class definition, add the following method to access the orders.csv file.

        public string dbPath()
        {
            //pull the directory path to the orders.csv file on the server.
            //this allows the file to be located even if the app is moved from server to server, or if using separate development and production servers.
            string workingDirectory = Environment.CurrentDirectory;
            string filePath = Directory.GetParent(workingDirectory).Parent.FullName + "\\App_Data";
            string orders = filePath + "\\orders.csv";

            return orders;
        }

Next, add the function below to parse the CSV data.

        public DataTable parseCsv()
        {
            //initialize a new DataTable object to store the parsed CSV data.
            ordersTable = new DataTable();
            //use the FileHelpers CommonEngine package to parse the data.
            ordersTable = CommonEngine.CsvToDataTable(dbPath(), ',');

            return ordersTable;
        }

Displaying Order Summaries

On the OrderSummaries page, two tables will display a summary of each order.

  • One table will include open orders, or orders with payments that have only been authorized.
  • The other table will include closed orders, or orders with payments that have been authorized and a subsequent action has also been performed, such as a void, capture, refund, etc.

To add the graphical table elements to the page, in the Solution Explorer right click on Form1.cs, and select View Designer. In the top navigation menu, select View, then Toolbox. In the Toolbox panel, select DataGridView, drag it onto Form1, and if prompted to add a data source, click on the background of the form to hide the prompt window because a data source won’t be added at this time. In the Properties panel, you can see that the DataGridView object was given a default name dataGridView1. From the Toolbox, select TextBox, drag it onto Form1 directly above dataGridView1, and in the Properties panel, update the Text property to “Open Orders”. You can see that the TextBox object was given a default name textBox1.

Follow the process described in the previous paragraph again, and place the second DataGridView and TextBox objects to the right of the first ones. Update the TextBox’s Text property to “Closed Orders”.

Navigate back to the Code View of Form1.cs and below the parseCsv method, add the following code.

public void updateOrders()
{
    //define the columns that we want to display on the OrderSummaries page.
    string[] selectedColumns = new[] { "Order", "Employee", "Table", "Total", "Status" };
    //create a data view to display only the columns defined above.
    DataTable filteredColsView = new DataView(parseCsv()).ToTable(false, selectedColumns);

    //create a data view to display open orders.
    DataView openOrders = new DataView(filteredColsView);
    openOrders.RowFilter = "Status='Open' OR Status='Amount Adjusted'";
    dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
    //bind this as the data source for dataGridView1.
    dataGridView1.DataSource = openOrders;

    //create a data view to display only closed orders.
    DataView closedOrders = new DataView(filteredColsView);
    closedOrders.RowFilter = "Status='Captured' OR Status='Batched' OR Status='Voided' OR Status='Refunded'";
    dataGridView2.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
    //bind this as the data source for dataGridView2.
    dataGridView2.DataSource = closedOrders;
}

The updateOrders method populates each of the DataGridView tables with the current data from the database.

Creating a New Order

In addition to displaying order information, the OrderSummaries page will include a “Start New Order” button that generates a new, empty record in the database and opens the OrderDetails page, where users can add items to the order.

Open Form1.cs in Design View, and from the Toolbox, drag a Button onto the page in any location you choose. Update the button’s Text property to “Start New Order”. Double click on the button, and a new method will be added automatically in Code View. Rename the method newOrderButtonClick and leave the default definition for now.

After the newOrderButtonClick method, add the following code.

        public string createNewOrder()
        {
            //call the dbPath method to access orders.csv.
            string ordersFile = dbPath();
            //get the order number of the last record.
            string lastLine = File.ReadLines(ordersFile).Last();
            string[] cellValues = lastLine.Split(',');
            string lastOrderValue;
            //check if there are existing records. if not, the first cell value will be equal to the first header value, which is "Order".
            if (cellValues.First() != "Order") { lastOrderValue = cellValues.First(); } else { lastOrderValue = "0"; }
            //increment the most recent order number by 1.
            string newOrderValue = (Int32.Parse(lastOrderValue) + 1).ToString();

            //add a new record to the database with the following default values:
            //order # = Auto-incremented number
            //status = Open
            using (StreamWriter w = new StreamWriter(ordersFile, true))
            {
                w.WriteLine(newOrderValue + ", , ,Open,0.00, ,0.00,0.00,0.00,0,0, , ,0.00");
            }

            //update the gridview with the new order.
            updateOrders();

            return newOrderValue;
        }

The createNewOrder method adds a new, empty record to the database, generates a new order number by incrementing the previous order number by 1, calls updateOrders to refresh the data displayed in the DataGridView tables, and returns the new order number.

Add the following code to the newOrderButtonClick method definition that was created previously.

        public void newOrderButtonClick(object sender, EventArgs e)
        {
            //call the createNewOrder method to create a new empty record and obtain a new order number
            string newOrderNumber = createNewOrder().ToString();
            //use a linq query to select the data table row where the order number is equal to the new order number.
            var query =
                from order in parseCsv().AsEnumerable()
                where order.Field<string>("Order") == newOrderNumber
                select order;

            //initialize itemDictionary as a new, empty dictionary which will be filled in OrderDetail, and will include all items that are added to an order.
            itemDictionary = new Dictionary<string, decimal>();

            //create a new dataview to execute the query and display the result.
            DataView newOrder = query.AsDataView();
            selectedOrder = newOrder;
            //pass the dataview object to the displayRecordDetail method of the OrderDetail instance of the Form2 class.
            //this will display details of the new record on the OrderDetail screen.
            FormProvider.OrderDetail.displayRecordDetail();
            //show the OrderDetail instance of the Form2 class.
            FormProvider.OrderDetail.Show();
            //hide the OrderSummaries instance of the Form1 class.
            this.Hide();
        }

When the “Start New Order” button is clicked, the createNewOrder method is called, and the new order number that’s returned is used in a linq query to select that record from the database and create a new DataView with that record’s data. The DataView is passed to the OrderDetail instance of the Form2 class, OrderDetail is displayed, and OrderSummaries is hidden.

Displaying a Selected Order

Clicking an existing order on the OrderSummaries page will open the OrderDetails page and display all data for that order. To add this functionality, place the following code below the newOrderButtonClick method definition.

       private void displayRecordSummary(object selectedOrderNumber)
       {
           //use a linq query to select the data table row where the order number is equal to the order number of the selected gridview row.
           var query =
               from order in parseCsv().AsEnumerable()
               where order.Field<string>("Order") == selectedOrderNumber.ToString()
               select order;

           //create a new dataview to execute the query and display the result.
           selectedOrder = query.AsDataView();

            //since the order items are stored in the database as a JSON object, deserialize the object and set the result as the value of itemDictionary.
           var json = selectedOrder[0]["Items"].ToString().Replace(';', ',');
           itemDictionary = JsonConvert.DeserializeObject<Dictionary<string, decimal>>(json);

           //pass the dataview object to the displayRecordDetail method of the OrderDetail instance of the Form2 class.
          //this will display details of the selected record on the OrderDetail screen.
           FormProvider.OrderDetail.displayRecordDetail();
           //show the OrderDetail instance of the Form2 class.
           FormProvider.OrderDetail.Show();
           //hide the OrderSummaries instance of the Form1 class.
           this.Hide();
       }

In the Design View of Form1.cs, double-click on dataGridView1 to generate a new method definition in Code View named dataGridView1_CellContentClick. Do the same for dataGridView2. Add the following code to each, changing dataGridView1 to dataGridView2 in that definition.

        private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
        {
            //get the order number of the selected order and call displayRecordSummary, passing the order number as an argument.
            object selectedOrderNumber = dataGridView1.SelectedRows[0].Cells[0].Value;
            displayRecordSummary(selectedOrderNumber);
        }

Adding a Batch Close Button

The last functionality that the OrderSummaries page will handle is closing a batch of transactions. In Design View, drag a new Button from the Toolbox onto the UI near the “Start New Order” button and update the Text property to “Close Batch”. Double-click it to generate a new method definition in Code View and leave the default definition for now.

POS Page 2: Order Details

At the beginning of the file, add the following Using statements to access the required .NET namespaces. If your environment is missing any, select Project from the top menu, then Manage NuGet Packages, search for the necessary package, and install.

using System;
using System.IO;
using System.Windows.Forms;
using System.Linq;
using Newtonsoft.Json;

Within the Form2 class definition that’s provided by default, add the following code to initialize class-level variables.

        //initialize a public class-level string to store the payment status that will be returned from the processor in Part 2 of this tutorial.
        public string labelText
        {
            get { return label1.Text; }
            set { label1.Text = value; }
        }

        //instantiate a public class-level string to store the payment response that will be returned from the processor and will be used for transaction validation.
        public string response { get; set; }

        //instantiate a public class-level string to store the payment authorization token that will be returned from the processor.
        public string authToken { get; set; }

        //initialize a public class-level string to track the payment transaction token that will be returned from the processor.
        public string transactionToken {  get; set; }

Setting Up the UI

On the OrderDetails page, all information related to a record will be displayed, and users will be able to add items to the order and perform various payment functions. To organize this functionality, the left half of the page will display information about the order and the right half will include the buttons to add items and perform payment functions.

Open Form2.cs in Design View and drag and drop tools from the Toolbox onto the UI to build a page with the following elements. Tool suggestions and the corresponding names that are used in this tutorial are shown in red text below. After adding the buttons to the UI, double-click each to generate a new method definition in Code View and leave the default definitions for now.

Tools used to build the POS UI
Names of elements used to build the POS UI

Reading and Writing Order Details

Open Form2.cs in Code View and below the class-level variables, add the following method definition. When called, this method will display the selected order’s data in the TextBoxes on the UI.

        public void displayRecordDetail()
        {
            //assign values from the selectedOrder dataview to the corresponding TextBox values.
            orderNumValue.Text = (string)FormProvider.OrderSummaries.selectedOrder[0]["Order"];
            tableNumValue.Text = (string)FormProvider.OrderSummaries.selectedOrder[0]["Table"];
            serverNumValue.Text = (string)FormProvider.OrderSummaries.selectedOrder[0]["Employee"];
            statusValue.Text = (string)FormProvider.OrderSummaries.selectedOrder[0]["Status"];

            //in a real-world scenario that uses a full-featured database, the following currency values can be easily stored as decimals,
            //eliminating the need to convert to and from strings.
            //since this tutorial uses a flat-file csv database, all values are being stored as strings.
            subtotalValue.Text = string.Format("{0:C}", FormProvider.OrderSummaries.selectedOrder[0]["Subtotal"]);
            taxValue.Text = string.Format("{0:C}", FormProvider.OrderSummaries.selectedOrder[0]["Tax"]);
            tipValue.Text = string.Format("{0:C}", FormProvider.OrderSummaries.selectedOrder[0]["Tip"]);
            totalValue.Text = string.Format("{0:C}", FormProvider.OrderSummaries.selectedOrder[0]["Total"]);
            authAmtValue.Text = string.Format("{0:C}", FormProvider.OrderSummaries.selectedOrder[0]["AuthAmount"]);

            //bind itemDictionary as the data source for listBox1.
            //itemDictionary includes all items that have been added to an order.
            listBox1.DataSource = new BindingSource(FormProvider.OrderSummaries.itemDictionary, null);
            listBox1.DisplayMember = "Key" + "Value";
        }

Next, add the following method to edit database records. It accepts three parameters:

  1. The updated text that will replace the existing text
  2. The name of the database file
  3. The index of the database record that will be edited
        static void editRecord(string newText, string fileName, int recordToEdit)
        {
            //save all rows in the csv to an array of strings.
            string[] allRecords = File.ReadAllLines(fileName);
            //save the new text to the string in the array with index equal to the recordToEdit argument.
            allRecords[recordToEdit - 1] = newText;
            //rewrite the file with the new data.
            File.WriteAllLines(fileName, allRecords);
        }

The editRecord method will be called in the next method, updateDb.

        public void updateDb()
        {
	    //serialize the dictionary that stores the order items to a JSON object.
            var dictionaryToJson = JsonConvert.SerializeObject(FormProvider.OrderSummaries.itemDictionary).Replace(',', ';');

            //call the editRecord method to apply the changes.
            //to access the csv file, pass as the filename argument the result of calling the dbPath method of the OrderSummaries instance of the Form1 class.
            //use the order number value + 1 for the record index, adding 1 to account for the header row.
            editRecord(orderNumValue.Text + "," +
                serverNumValue.Text + "," +
                tableNumValue.Text + "," +
                statusValue.Text + "," +
                totalValue.Text + "," + dictionaryToJson + "," +
                subtotalValue.Text + "," +
                taxValue.Text + "," +
                tipValue.Text + "," +
                authIdValue.Text + "," +
                transactionIdValue.Text + "," +
                FormProvider.OrderSummaries.batchId + "," +
                authToken + "," +
                transactionToken + "," +
                authAmtValue.Text, FormProvider.OrderSummaries.dbPath(), Int32.Parse(orderNumValue.Text) + 1);
        }

Next, add a method to update the order details on the UI. For this tutorial, the logic to update the order on the UI and update the database are separate.

        public void updateOrder()
        {
            listBox1.DataSource = new BindingSource(FormProvider.OrderSummaries.itemDictionary, null);
            listBox1.DisplayMember = "Key" + "Value";
	    //set a variable named subTotal equal to the result of adding all decimal values in the itemDictionary, then display it in the subtotalValue text box on the UI.
            decimal subTotal = FormProvider.OrderSummaries.itemDictionary.Sum(x => x.Value);
            subtotalValue.Text = string.Format("{0:C}", subTotal);
	    //calculate any taxes required in that region and display it in the taxValue text box on the UI.
            decimal tax = .06m * subTotal;
            taxValue.Text = string.Format("{0:C}", tax);
	    //set a variable named tip equal to the decimal amount entered in the tipValue text box on the UI.
            decimal tip = decimal.Parse(tipValue.Text);
	    //add the subtotal, tax, and tip amounts, and display the total in the totalValue text box on the UI.
            totalValue.Text = string.Format("{0:C}", (tax + subTotal + tip));
            transactionIdValue.Text = FormProvider.OrderSummaries.transactionId;
        }

The next methods are used to add and remove items from the itemDictionary. They accept two parameters:

  1. Item name
  2. Item cost

In the addItem method, if itemDictionary already includes the item to be added, only the cost will be updated. In the removeItem method, if itemDictionary doesn’t include the item to be removed, the method is exited without taking any action.

Notice that once an item is added, the authId is displayed on the UI, indicating that a payment method can now be performed on the order.

        public void addItem(string item, decimal amount)
        {
	    //if itemDictionary already includes the item to be added, only update the cost of the existing dictionary item.
            if (FormProvider.OrderSummaries.itemDictionary.ContainsKey(item) == true)
            {
                FormProvider.OrderSummaries.itemDictionary[item] = FormProvider.OrderSummaries.itemDictionary[item] + amount;
            }
	    //if itemDictionary doesn’t already include the item to be added, add it as a new dictionary item.
            else { FormProvider.OrderSummaries.itemDictionary.Add(item, amount); }
            updateOrder();
            authIdValue.Text = FormProvider.OrderSummaries.authId;
        }

        public void removeItem(string item, decimal amount)
        {
          //if itemDictionary doesn’t include the item to be removed, exit the method without taking any action.
            if (FormProvider.OrderSummaries.itemDictionary.ContainsKey(item) != true)
            {
                return;
            }
          //if itemDictionary includes multiple instances of the item to be removed, decrease the amount by the cost of a single item.
            else if (FormProvider.OrderSummaries.itemDictionary[item] != amount)
            {
                FormProvider.OrderSummaries.itemDictionary[item] = FormProvider.OrderSummaries.itemDictionary[item] - amount;
            }
	    //if itemDictionary includes one instance of the item to be removed, remove the item from the dictionary.
            else { FormProvider.OrderSummaries.itemDictionary.Remove(item); }
            updateOrder();
            authIdValue.Text = FormProvider.OrderSummaries.authId;
        }

For each of the buttons that add or remove items on an order, add the relevant call to addItem or removeItem with the required arguments. For example:

private void addSmCoffeeButton_Click(object sender, EventArgs e)
{
    addItem("Small Coffee", 1.00m);
}

private void removeSmCoffeeButton_Click(object sender, EventArgs e)
{
    removeItem("Small Coffee", 1.00m);
}

Finally, add the updateOrderStatus method, which will be called from the CommonPayment class that will be created in Part 2 of this tutorial. The updateOrderStatus method will be used to update the status of an order after a transaction has been processed.

        public void updateOrderStatus(string newStatus)
        {
            statusValue.Text = newStatus;
            updateDb();
        }

Demonstrating the POS App

Click the Start button in Visual Studio Community to run the application. The OrderSummaries page will load by default. Click the Start New Order button.
Point of Sale Application
The OrderDetails page should open, displaying an empty order. Enter data in the textbox fields, and use the buttons add items to the order. Click the Back button, and the order data should be displayed on the OrderSummaries page.
Point of Sale Application Payment Functionality

Conclusion

In Part 1 of this tutorial, you learned how to build a simple POS application to create and update customer orders using C# with the .NET framework. In Part 2, you’ll learn how to use the PAX Semi-Integrated SDK to integrate credit card acceptance into the POS.

How To Get Started

North’s Sales Engineering team provides support to developers and business decision-makers to help select the best possible payment system. Contact us to learn more about how to connect your system to the North ecosystem.


Start your free Developer account and try it now.


©2025 North is a registered DBA of NorthAB, LLC. All rights reserved. North is a registered ISO of BMO Harris Bank N.A., Chicago, IL, Citizens Bank N.A., Providence, RI, The Bancorp Bank, Philadelphia, PA, FFB Bank, Fresno, CA, Wells Fargo Bank, N.A., Concord, CA, and PNC Bank, N.A.