How to Build a Shopping Cart with APEX Collections in Oracle APEX

An APEX collection holds the cart per session, cards add products, an editable grid changes quantities, and a checkout turns the cart into an order.

A shopping cart holds what a user picked before anything is saved as an order. The rows belong to one user and one visit, they change all the time, and most carts are never checked out. Writing them into the order tables on every click leaves half-finished orders behind.

APEX collections fit this job. A collection is a set of rows stored per APEX session, so every user gets a private cart without an extra table. This guide builds a product page with Add to Cart buttons, a cart page where users change quantities and remove items, and a checkout that turns the cart into an order.

The posts How to Store Temporary Data in an APEX Session Using APEX_COLLECTION and Oracle APEX Collection Example explain the APEX_COLLECTION API call by call. This guide uses a few of those calls to build the complete cart: pages, buttons, an editable grid on the collection, validations and the checkout.

The guide uses products and orders as the example. The same steps work for any "pick now, save later" list, such as items for a quote, books to borrow or tasks to assign.

Step 1: Create the Tables

The cart itself needs no table, but the products and the orders do:

  • CART_PRODUCTS holds the products with their price and an IS_ACTIVE flag.
  • CART_ORDERS holds one row per checkout, with the user and the date.
  • CART_ORDER_LINES holds the products of each order, with the quantity and the price at checkout.

Create the tables and six products. The wall calendar is no longer sold:

Example:

create table cart_products (
  product_id   number generated by default on null as identity,
  sku          varchar2(20)   not null,
  name         varchar2(100)  not null,
  description  varchar2(400),
  unit_price   number(10,2)   not null,
  is_active    varchar2(1)    default on null 'Y' not null,
  constraint cart_products_pk primary key (product_id),
  constraint cart_products_sku_uk unique (sku),
  constraint cart_products_price_ck check (unit_price > 0),
  constraint cart_products_active_ck check (is_active in ('Y', 'N'))
);

create table cart_orders (
  order_id    number generated by default on null as identity,
  ordered_by  varchar2(128)  default on null
                coalesce(sys_context('APEX$SESSION', 'APP_USER'), user) not null,
  ordered_on  date           default on null sysdate not null,
  status      varchar2(10)   default on null 'NEW' not null,
  constraint cart_orders_pk primary key (order_id),
  constraint cart_orders_status_ck check (status in ('NEW', 'PAID', 'SHIPPED', 'CANCELLED'))
);

create table cart_order_lines (
  line_id     number generated by default on null as identity,
  order_id    number         not null,
  product_id  number         not null,
  quantity    number(3)      not null,
  unit_price  number(10,2)   not null,
  line_total  number(12,2)   generated always as (quantity * unit_price) virtual,
  constraint cart_order_lines_pk primary key (line_id),
  constraint cart_order_lines_order_fk foreign key (order_id)
    references cart_orders (order_id) on delete cascade,
  constraint cart_order_lines_product_fk foreign key (product_id)
    references cart_products (product_id),
  constraint cart_order_lines_uk unique (order_id, product_id),
  constraint cart_order_lines_qty_ck check (quantity between 1 and 99)
);

create index cart_order_lines_product_ix on cart_order_lines (product_id);

insert into cart_products (sku, name, description, unit_price)
values ('NB-A5', 'A5 Notebook', 'Dotted pages, 160 sheets, hard cover.', 6.50);

insert into cart_products (sku, name, description, unit_price)
values ('PEN-GEL', 'Gel Pen Set', 'Ten colours, 0.5 mm tips.', 8.90);

insert into cart_products (sku, name, description, unit_price)
values ('MUG-CER', 'Ceramic Mug', 'Holds 350 ml, dishwasher safe.', 12.00);

insert into cart_products (sku, name, description, unit_price)
values ('BAG-TOT', 'Canvas Tote Bag', 'Cotton canvas with a front pocket.', 15.75);

insert into cart_products (sku, name, description, unit_price)
values ('LAMP-LED', 'LED Desk Lamp', 'Three brightness levels and a USB port.', 34.00);

insert into cart_products (sku, name, description, unit_price, is_active)
values ('CAL-2025', 'Wall Calendar 2025', 'Last year''s calendar, no longer sold.', 9.00, 'N');

commit;

select product_id, sku, name, unit_price, is_active
  from cart_products
 order by product_id;

Output:

Table CART_PRODUCTS created.

Table CART_ORDERS created.

Table CART_ORDER_LINES created.

Index CART_ORDER_LINES_PRODUCT_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

   PRODUCT_ID SKU         NAME                     UNIT_PRICE IS_ACTIVE
_____________ ___________ _____________________ _____________ ____________
            1 NB-A5       A5 Notebook                     6.5 Y
            2 PEN-GEL     Gel Pen Set                     8.9 Y
            3 MUG-CER     Ceramic Mug                      12 Y
            4 BAG-TOT     Canvas Tote Bag               15.75 Y
            5 LAMP-LED    LED Desk Lamp                    34 Y
            6 CAL-2025    Wall Calendar 2025                9 N

6 rows selected.

The rules that can be declared are in the tables, not in code:

  • ORDERED_BY uses DEFAULT ON NULL with the APEX user from sys_context('APEX$SESSION', 'APP_USER'). Outside APEX it falls back to the database user.
  • ORDERED_ON and STATUS also use DEFAULT ON NULL, so the checkout only inserts the status.
  • LINE_TOTAL is a virtual column, so it is always the quantity times the price.
  • CART_ORDER_LINES_UK allows each product once per order, and CART_ORDER_LINES_QTY_CK keeps the quantity between 1 and 99.
  • CART_ORDER_LINES_PRODUCT_IX indexes the product foreign key. The order foreign key is the first column of the unique key.

Step 2: Create the Cart Package

The cart lives in a collection named SHOPPING_CART. Each member is one product: N001 holds the product ID and N002 the quantity. The name and price are not copied into the collection. The pages join CART_PRODUCTS, so they always show the current price.

A small package keeps all cart logic in one place, so the page processes stay one line long:

Example:

create or replace package cart_pkg as
  c_cart constant varchar2(30) := 'SHOPPING_CART';
  procedure add_item (p_product_id in number, p_quantity in number default 1);
  procedure set_quantity (p_seq_id in number, p_quantity in number);
  procedure remove_item (p_seq_id in number);
  function item_count return number;
  function checkout return number;
end cart_pkg;
/

create or replace package body cart_pkg as

  procedure add_item (p_product_id in number, p_quantity in number default 1) is
    l_active  cart_products.is_active%type;
    l_seq_id  number;
    l_qty     number;
  begin
    select max(is_active) into l_active
      from cart_products
     where product_id = p_product_id;
    if l_active is null or l_active <> 'Y' then
      raise_application_error(-20001, 'This product is not available.');
    end if;

    if not apex_collection.collection_exists(c_cart) then
      apex_collection.create_collection(c_cart);
    end if;

    select max(seq_id), max(n002) into l_seq_id, l_qty
      from apex_collections
     where collection_name = c_cart
       and n001 = p_product_id;

    if l_seq_id is null then
      apex_collection.add_member(
        p_collection_name => c_cart,
        p_n001            => p_product_id,
        p_n002            => p_quantity);
    else
      set_quantity(l_seq_id, l_qty + p_quantity);
    end if;
  end add_item;

  procedure set_quantity (p_seq_id in number, p_quantity in number) is
  begin
    if p_quantity is null or p_quantity not between 1 and 99 then
      raise_application_error(-20002, 'Quantity must be between 1 and 99.');
    end if;
    apex_collection.update_member_attribute(
      p_collection_name => c_cart,
      p_seq             => p_seq_id,
      p_attr_number     => 2,
      p_number_value    => p_quantity);
  end set_quantity;

  procedure remove_item (p_seq_id in number) is
  begin
    apex_collection.delete_member(
      p_collection_name => c_cart,
      p_seq             => p_seq_id);
  end remove_item;

  function item_count return number is
    l_count number;
  begin
    select nvl(sum(n002), 0) into l_count
      from apex_collections
     where collection_name = c_cart;
    return l_count;
  end item_count;

  function checkout return number is
    l_order_id     cart_orders.order_id%type;
    l_unavailable  number;
  begin
    if item_count = 0 then
      raise_application_error(-20003, 'Your cart is empty.');
    end if;

    select count(*) into l_unavailable
      from apex_collections c
      left join cart_products p
        on p.product_id = c.n001
       and p.is_active = 'Y'
     where c.collection_name = c_cart
       and p.product_id is null;
    if l_unavailable > 0 then
      raise_application_error(-20004,
        'Some products in your cart are no longer available. Remove them and try again.');
    end if;

    insert into cart_orders (status) values ('NEW')
    returning order_id into l_order_id;

    insert into cart_order_lines (order_id, product_id, quantity, unit_price)
    select l_order_id, p.product_id, c.n002, p.unit_price
      from apex_collections c
      join cart_products p on p.product_id = c.n001
     where c.collection_name = c_cart;

    apex_collection.truncate_collection(c_cart);
    return l_order_id;
  end checkout;

end cart_pkg;
/

select object_name, object_type, status
  from user_objects
 where object_name = 'CART_PKG'
 order by object_type;

Output:

Package CART_PKG compiled

Package Body CART_PKG compiled

OBJECT_NAME    OBJECT_TYPE     STATUS
______________ _______________ _________
CART_PKG       PACKAGE         VALID
CART_PKG       PACKAGE BODY    VALID

What each part does:

  • ADD_ITEM refuses unknown and inactive products. It creates the collection on first use. If the product is already in the cart, it raises the quantity instead of adding a second member.
  • SET_QUANTITY changes N002 with APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE. Attribute number 2 with P_NUMBER_VALUE means column N002.
  • REMOVE_ITEM deletes one member by its sequence ID.
  • ITEM_COUNT returns the number of items, which is the sum of the quantities.
  • CHECKOUT refuses an empty cart and products that are no longer sold. It inserts the order and its lines with the current prices, empties the cart and returns the new order ID.

Step 3: Test the Package in SQL

APEX_COLLECTION needs an APEX session. APEX_SESSION.CREATE_SESSION creates one for application 400 and the user DEMO_DEV, so the package can be tested in SQL. The test adds the notebook twice, changes and removes members, tries an inactive product and a quantity of 0, then checks out twice:

Example:

set serveroutput on
begin
  apex_session.create_session(p_app_id => 400, p_page_id => 1, p_username => 'DEMO_DEV');
  cart_pkg.add_item(1);
  cart_pkg.add_item(3);
  cart_pkg.add_item(1);
  cart_pkg.add_item(5, 2);
  dbms_output.put_line('Items in cart: ' || cart_pkg.item_count);
end;
/

select c.seq_id, p.name, c.n002 as quantity, p.unit_price,
       c.n002 * p.unit_price as line_total
  from apex_collections c
  join cart_products p on p.product_id = c.n001
 where c.collection_name = 'SHOPPING_CART'
 order by c.seq_id;

begin
  cart_pkg.set_quantity(2, 4);
  cart_pkg.remove_item(3);
  dbms_output.put_line('Items in cart: ' || cart_pkg.item_count);
end;
/

select c.seq_id, p.name, c.n002 as quantity, p.unit_price,
       c.n002 * p.unit_price as line_total
  from apex_collections c
  join cart_products p on p.product_id = c.n001
 where c.collection_name = 'SHOPPING_CART'
 order by c.seq_id;

begin
  cart_pkg.add_item(6);
end;
/

begin
  cart_pkg.set_quantity(1, 0);
end;
/

declare
  l_order_id number;
begin
  l_order_id := cart_pkg.checkout;
  dbms_output.put_line('Order ' || l_order_id || ' created. Items left in cart: ' || cart_pkg.item_count);
end;
/

select o.order_id, o.ordered_by, o.status, p.name, l.quantity, l.unit_price, l.line_total
  from cart_orders o
  join cart_order_lines l on l.order_id = o.order_id
  join cart_products p on p.product_id = l.product_id
 order by o.order_id, l.line_id;

declare
  l_order_id number;
begin
  l_order_id := cart_pkg.checkout;
end;
/

begin
  apex_session.delete_session;
end;
/

Output:

Items in cart: 5

PL/SQL procedure successfully completed.

   SEQ_ID NAME                QUANTITY    UNIT_PRICE    LINE_TOTAL
_________ ________________ ___________ _____________ _____________
        1 A5 Notebook                2           6.5            13
        2 Ceramic Mug                1            12            12
        3 LED Desk Lamp              2            34            68

Items in cart: 6

PL/SQL procedure successfully completed.

   SEQ_ID NAME              QUANTITY    UNIT_PRICE    LINE_TOTAL
_________ ______________ ___________ _____________ _____________
        1 A5 Notebook              2           6.5            13
        2 Ceramic Mug              4            12            48

begin
*
ERROR at line 1:
ORA-20001: This product is not available.
ORA-06512: at "APXSOL.CART_PKG", line 12
ORA-06512: at line 2
Help: https://docs.oracle.com/error-help/db/ora-20001/

begin
*
ERROR at line 1:
ORA-20002: Quantity must be between 1 and 99.
ORA-06512: at "APXSOL.CART_PKG", line 37
ORA-06512: at line 2
Help: https://docs.oracle.com/error-help/db/ora-20002/

Order 1 created. Items left in cart: 0

PL/SQL procedure successfully completed.

   ORDER_ID ORDERED_BY    STATUS    NAME              QUANTITY    UNIT_PRICE    LINE_TOTAL
___________ _____________ _________ ______________ ___________ _____________ _____________
          1 DEMO_DEV      NEW       A5 Notebook              2           6.5            13
          1 DEMO_DEV      NEW       Ceramic Mug              4            12            48

declare
*
ERROR at line 1:
ORA-20003: Your cart is empty.
ORA-06512: at "APXSOL.CART_PKG", line 67
ORA-06512: at line 4
Help: https://docs.oracle.com/error-help/db/ora-20003/

PL/SQL procedure successfully completed.

The second notebook raised the quantity of member 1 to 2 instead of adding a new member. The checkout created order 1 with the APEX user DEMO_DEV, and the empty cart refused a second checkout.

Step 4: Create the Shop Page

In App Builder, open the application and click Create Page. Select the Blank Page tile:

Create a Page wizard with the Blank Page tile marked with a red box
Select the Blank Page type.

Enter 750 as the Page Number and Shop as the Name. Turn off Use Breadcrumb, keep Use Navigation on, enter fa-shopping-cart as the Icon and click Create Page:

Create Blank Page screen with Page Number 750, Name Shop, Use Breadcrumb off, Use Navigation on and Icon fa-shopping-cart marked with red boxes
The shop page gets a navigation menu entry.

Step 5: Show the Cart Count

In Page Designer, create a region named Your Cart with the Type Static Content:

Region Your Cart with Name and Type Static Content marked with red boxes
The region shows the number of items and a link to the cart.

Add a page item P750_CART_COUNT to the Your Cart region. Set the Type to Display Only and the Label to Items in Cart:

Page item P750_CART_COUNT with Name, Type Display Only and Label Items in Cart marked with red boxes
A read-only counter.

Fill the item with a computation. In the Rendering tree, right-click Pre-Rendering > Before Header and create a computation for P750_CART_COUNT. Set the Type to Expression, the Language to PL/SQL and the PL/SQL Expression to:

Example:

cart_pkg.item_count
Computation for P750_CART_COUNT with Item Name, Point Before Header, Type Expression and the PL/SQL Expression marked with red boxes
The count is read each time the page loads.

Add a button VIEW_CART with the Label View Cart. Put it in the Your Cart region, slot Next, and turn on Hot:

Button VIEW_CART with Button Name, Label View Cart, Region Your Cart, Slot Next and Hot on marked with red boxes
The button sits at the top right of the region.

In the Behavior group, set the Action to Redirect to Page in this Application and the Target to page 751. Page 751 is the cart page created in Step 8:

Behavior Action Redirect to Page and Target Page 751 marked with a red box
View Cart opens the cart page.

Step 6: Show the Products as Cards

Create a region named Products with the Type Cards. Set the Source Type to SQL Query and enter this query. It shows only active products and formats the price with the currency symbol:

Example:

select product_id,
       name,
       sku,
       description,
       to_char(unit_price, 'FML999G990D00') as price
  from cart_products
 where is_active = 'Y'
Region Products with Name, Type Cards, Source Type SQL Query and the query marked with red boxes
Inactive products never show up.

In the Order By group, set the Type to Static Value and the Order By Clause to name:

Order By Type Static Value and Order By Clause name marked with a red box
The cards are sorted by name.

On the Attributes tab, set Primary Key Column 1 to PRODUCT_ID, the Title Column to NAME, the Subtitle Column to SKU and the Body Column to DESCRIPTION:

Cards attributes with Primary Key Column 1 PRODUCT_ID, Title Column NAME, Subtitle Column SKU and Body Column DESCRIPTION marked with red boxes
What each card shows.

In the Icon and Badge group, set the Badge Column to PRICE and the Badge Label to Price:

Badge Column PRICE and Badge Label Price marked with a red box
The price appears as a badge on each card.

Step 7: Add the Add to Cart Button

Add a hidden page item P750_PRODUCT_ID to the page and turn off Value Protected. The button sets this item in the browser, and a protected hidden item would be refused with a checksum error:

Page item P750_PRODUCT_ID with Name, Type Hidden and Value Protected off marked with red boxes
The item carries the product ID to the server.

In the Rendering tree, right-click the Actions node under Products and click Create Action. Set the Type to Button and the Label to Add to Cart. In the Behavior group, set the Type to Redirect to URL and enter this Target URL:

Example:

javascript:apex.page.submit({request: 'ADD', set: {'P750_PRODUCT_ID': '&PRODUCT_ID.'}});
Card action with Type Button, Label Add to Cart, Behavior Type Redirect to URL and the Target URL marked with red boxes
The button submits the page with the request ADD.

&PRODUCT_ID. is replaced with the product of each card. apex.page.submit sets P750_PRODUCT_ID and posts the page with the request ADD. The add runs in a POST, so a reload or a bookmark never adds the product again.

In the Appearance group, set the Display Type to Text with Icon, the Icon to fa-cart-plus and turn on Hot:

Card action Display Type Text with Icon, Icon fa-cart-plus and Hot on marked with a red box
A cart icon on each button.

Switch to the Processing tab and create a process named Add to Cart with the Type Execute Code. Enter the PL/SQL Code:

Example:

begin
    cart_pkg.add_item(p_product_id => :P750_PRODUCT_ID);
end;
Process Add to Cart with Name, Type Execute Code and the PL/SQL Code marked with red boxes
The process calls the package.

Set the Success Message to Added to your cart. and the Error Message to #SQLERRM_TEXT#. The substitution shows only the text of the package error, without the ORA- number. Under Server-side Condition, set the Type to Request = Value and the Value to ADD:

Success Message Added to your cart, Error Message #SQLERRM_TEXT# and Server-side Condition Type Request = Value with Value ADD marked with red boxes
The process runs only for the request ADD.

Create a branch named Back to Shop under After Processing. Keep the Type Page or URL (Redirect) and set the Target to page 750:

Branch Back to Shop with Name, Type Page or URL and Target Page 750 marked with red boxes
After the add, the shop page loads again with the new count.

Step 8: Create the Cart Page

Click Create Page again and select the Blank Page tile, as in Step 4. Enter 751 as the Page Number and Shopping Cart as the Name. Turn off Use Breadcrumb and Use Navigation, because the View Cart button opens this page:

Create Blank Page screen with Page Number 751, Name Shopping Cart, Use Breadcrumb off and Use Navigation off marked with red boxes
The cart page has no menu entry.

Step 9: Show the Cart in an Editable Grid

Create a region named Cart Items with the Type Interactive Grid. Set the Source Type to SQL Query and enter this query on APEX_COLLECTIONS:

Example:

select c.seq_id,
       p.name,
       case p.is_active when 'Y' then 'In stock' else 'No longer sold' end as availability,
       p.unit_price,
       c.n002 as quantity,
       c.n002 * p.unit_price as line_total
  from apex_collections c
  join cart_products p on p.product_id = c.n001
 where c.collection_name = 'SHOPPING_CART'
Region Cart Items with Name, Type Interactive Grid, Source Type SQL Query and the query marked with red boxes
The grid reads the collection of the current session.

APEX_COLLECTIONS shows only the members of the current session, so the query needs no user filter. AVAILABILITY tells the user which product stopped being sold after it was added.

On the Attributes tab, turn on Edit Enabled. Under Allowed Operations, uncheck Add Row and keep Update Row and Delete Row. Under Toolbar Controls, keep only Actions Menu, because page buttons save the cart:

Edit Enabled on, Allowed Operations Update Row and Delete Row, and Toolbar Controls Actions Menu marked with red boxes
Users can change and delete rows, not add them.

Set Messages > When No Data Found to Your cart is empty.:

When No Data Found Your cart is empty. marked with a red box
The message for an empty cart.

Now set the columns. Expand Cart Items > Columns and click SEQ_ID. Set the Type to Hidden and turn on Primary Key. SEQ_ID is the key the package needs to update or remove a member:

Column SEQ_ID with Type Hidden and Primary Key on marked with red boxes
The sequence ID identifies each cart row.

Click NAME, set the Type to Display Only and the Heading to Product:

Column NAME with Type Display Only and Heading Product marked with red boxes
The product name cannot be edited.

Set AVAILABILITY to Display Only too. Click UNIT_PRICE, set the Type to Display Only and the Heading to Unit Price:

Column UNIT_PRICE with Type Display Only and Heading Unit Price marked with red boxes
The price comes from CART_PRODUCTS.

In the Appearance group, set the Format Mask to FML999G990D00, which shows the currency symbol and two decimals:

Format Mask FML999G990D00 marked with a red box
A money format for the price.

Click LINE_TOTAL, set the Type to Display Only, the Heading to Line Total and the same Format Mask:

Column LINE_TOTAL with Type Display Only and Heading Line Total marked with red boxes
The line total is calculated in the query.

Click QUANTITY, the only editable column. Set the Type to Number Field, the Minimum Value to 1 and the Maximum Value to 99, the same range as the package and the check constraint:

Column QUANTITY with Type Number Field, Minimum Value 1 and Maximum Value 99 marked with red boxes
The grid checks the range before the page is sent.

Turn on Value Required:

Validation Value Required on marked with a red box
An empty quantity is refused.

Step 10: Add the Order Summary

Create a second region named Order Summary with the Type Static Content:

Region Order Summary with Name and Type Static Content marked with red boxes
The region holds the total and the buttons.

Add a page item P751_CART_TOTAL to Order Summary with the Type Display Only and the Label Cart Total:

Page item P751_CART_TOTAL with Name, Type Display Only and Label Cart Total marked with red boxes
The total of all lines.

Create a Before Header computation for P751_CART_TOTAL with the Type SQL Query (return single value):

Example:

select to_char(nvl(sum(c.n002 * p.unit_price), 0), 'FML999G990D00')
  from apex_collections c
  join cart_products p on p.product_id = c.n001
 where c.collection_name = 'SHOPPING_CART'
Computation for P751_CART_TOTAL with Item Name, Point Before Header, Type SQL Query return single value and the query marked with red boxes
The total uses the current prices too.

Add a hidden page item P751_ORDER_ID. Keep Value Protected on, because only the checkout process sets it:

Page item P751_ORDER_ID with Name and Type Hidden marked with a red box
The new order ID for the success message.

Add a button CONTINUE_SHOPPING with the Label Continue Shopping to Order Summary, slot Previous:

Button CONTINUE_SHOPPING with Button Name, Label Continue Shopping, Region Order Summary and Slot Previous marked with red boxes
The way back to the products.

Set its Action to Redirect to Page in this Application with the Target page 750:

Behavior Action Redirect to Page and Target Page 750 marked with a red box
Continue Shopping opens the shop page.

Add a button UPDATE_CART with the Label Update Cart, slot Next. Keep the Action Submit Page:

Button UPDATE_CART with Button Name, Label Update Cart, Region Order Summary and Slot Next marked with red boxes
Update Cart saves the grid changes.

Add a button CHECKOUT with the Label Checkout, slot Next, and turn on Hot. Keep the Action Submit Page:

Button CHECKOUT with Button Name, Label Checkout, Region Order Summary, Slot Next and Hot on marked with red boxes
Checkout saves the grid changes and places the order.

Step 11: Save Grid Changes to the Collection

The page buttons submit the page, and the grid sends its changed rows with the submit. A grid process writes them to the collection.

On the Processing tab, create a process named Save Cart Changes. Set the Type to Interactive Grid - Automatic Row Processing (DML) and the Editable Region to Cart Items. In the Target group, set the Target Type to PL/SQL Code and enter:

Example:

begin
    case :APEX$ROW_STATUS
        when 'U' then
            cart_pkg.set_quantity(p_seq_id => :SEQ_ID, p_quantity => :QUANTITY);
        when 'D' then
            cart_pkg.remove_item(p_seq_id => :SEQ_ID);
    end case;
end;
Process Save Cart Changes with Name, Type Interactive Grid Automatic Row Processing, Target Type PL/SQL Code and the code marked with red boxes
The grid process calls the package for each changed row.

:APEX$ROW_STATUS is U for a changed row and D for a deleted row. The grid has no Add Row, so C never arrives. :SEQ_ID and :QUANTITY are the column values of the row.

Set Lock Row to No, because a collection member is not a table row to lock. Set the Success Message to Your cart was updated.:

Lock Row No and Success Message Your cart was updated. marked with red boxes
The message shows only when rows changed.

Step 12: Add the Checkout

Create a validation named Cart Is Not Empty with the Type Expression and this PL/SQL Expression. Set the Error Message to Your cart is empty. Add products before you check out.:

Example:

cart_pkg.item_count > 0
Validation Cart Is Not Empty with Type Expression, the PL/SQL Expression and the Error Message marked with red boxes
A friendly message for an empty cart.

Under Server-side Condition, set When Button Pressed to CHECKOUT:

Server-side Condition When Button Pressed CHECKOUT marked with a red box
Update Cart does not need the check.

Create a process named Checkout with the Type Execute Code and the Sequence 20, so it runs after Save Cart Changes. Enter the PL/SQL Code:

Example:

begin
    :P751_ORDER_ID := cart_pkg.checkout;
end;
Process Checkout with Name, Type Execute Code and the PL/SQL Code marked with red boxes
The package creates the order and empties the cart.

Set the Success Message to Thank you. Your order &P751_ORDER_ID. was placed., the Error Message to #SQLERRM_TEXT# and When Button Pressed to CHECKOUT:

Success Message with the order ID, Error Message #SQLERRM_TEXT# and When Button Pressed CHECKOUT marked with red boxes
The message names the new order.

Create a branch named Back to Cart under After Processing with the Target page 751:

Branch Back to Cart with Name, Type Page or URL and Target Page 751 marked with red boxes
Both buttons come back to the cart page.

Step 13: Test the Cart

Run page 750 as DEMO_DEV. The shop shows the five active products, and the cart is empty. Click Add to Cart on the Ceramic Mug:

Shop page with five product cards, Items in Cart 0 and the Add to Cart button of the Ceramic Mug marked with a red box
The wall calendar is not listed, because it is inactive.

Add the A5 Notebook twice and the LED Desk Lamp once. Each click shows Added to your cart., and the counter goes up to 4:

Shop page with the message Added to your cart. and Items in Cart 4 marked with a red box
Four items after four clicks.

Click View Cart. The notebook is one row with the quantity 2, not two rows, and the total is $59.00:

Cart grid with Ceramic Mug 1, A5 Notebook 2 and LED Desk Lamp 1, the A5 Notebook row marked with a red box
The second add raised the quantity.

Double-click the Quantity of the Ceramic Mug and enter 3. Open the row menu of the LED Desk Lamp and click Delete Row. The grid marks both changes, but nothing is saved yet:

Cart grid with Ceramic Mug quantity 3 changed and LED Desk Lamp crossed out as deleted, both rows marked with a red box
Changes wait for a button.

Click Update Cart. The page shows Your cart was updated., the lamp is gone and the total is $49.00 (3 × 12.00 + 2 × 6.50):

Cart after the update with Ceramic Mug 3 and A5 Notebook 2 and Cart Total 49.00 marked with red boxes
The collection now holds two members.

Click Checkout. The page shows the order number, and the cart is empty:

Cart page with the message Thank you. Your order 2 was placed., an empty grid and Cart Total 0.00
The cart became order 2.

The order rows match the cart. Order 1 came from the SQL test in Step 3:

Example:

select o.order_id, o.ordered_by, o.status, p.name, l.quantity, l.unit_price, l.line_total
  from cart_orders o
  join cart_order_lines l on l.order_id = o.order_id
  join cart_products p on p.product_id = l.product_id
 order by o.order_id, l.line_id;

select o.order_id, count(*) as lines, sum(l.quantity) as items, sum(l.line_total) as order_total
  from cart_orders o
  join cart_order_lines l on l.order_id = o.order_id
 group by o.order_id
 order by o.order_id;

Output:

   ORDER_ID ORDERED_BY    STATUS    NAME              QUANTITY    UNIT_PRICE    LINE_TOTAL
___________ _____________ _________ ______________ ___________ _____________ _____________
          1 DEMO_DEV      NEW       A5 Notebook              2           6.5            13
          1 DEMO_DEV      NEW       Ceramic Mug              4            12            48
          2 DEMO_DEV      NEW       Ceramic Mug              3            12            36
          2 DEMO_DEV      NEW       A5 Notebook              2           6.5            13

   ORDER_ID    LINES    ITEMS    ORDER_TOTAL
___________ ________ ________ ______________
          1        2        6             61
          2        2        5             49

Step 14: Test the Error Cases

These tests ran as SALES_USER, while DEMO_DEV still had one item in the cart. The SALES_USER shop showed Items in Cart 0. Each session has its own collection, so carts never mix.

Checkout with an Empty Cart

Click View Cart and then Checkout. The validation stops the request before the checkout process runs:

Error Your cart is empty. Add products before you check out. and the empty grid message marked with a red box
An empty cart cannot become an order.

A Forced Request for an Inactive Product

The wall calendar has no card, but a user can still send the request from the browser console:

Example:

apex.page.submit({request: 'ADD', set: {'P750_PRODUCT_ID': '6'}});

The package checks IS_ACTIVE, so the product is refused, and the counter stays at 0:

Error This product is not available. on the shop page with Items in Cart 0
The check runs on the server, not only in the page.

A Product Withdrawn after It Was Added

Add the Canvas Tote Bag and the Gel Pen Set. Then set the bag to inactive in another session:

Example:

update cart_products set is_active = 'N' where sku = 'BAG-TOT';
commit;

Open the cart and click Checkout. The bag shows No longer sold, and the checkout is refused:

Error Some products in your cart are no longer available. Remove them and try again. with the Canvas Tote Bag row showing No longer sold marked with a red box
The Availability column shows what to remove.

No order was created. The bag was set back to active after this test.

A Quantity Out of Range

Change the quantity of the Gel Pen Set to 0 and click Update Cart. The number field stops the submit in the browser:

Error Quantity must be a number between 1 and 99. with the quantity cell 0 shown in error
The grid uses the Minimum Value and Maximum Value of the column.

If the browser check is bypassed, SET_QUANTITY still refuses the value with ORA-20002, as the SQL test in Step 3 showed.

Things to Know

  • A collection belongs to one APEX session. When the user signs out or the session expires, the cart is gone. To keep carts across visits, save them in a table instead.
  • The cart stores only product IDs and quantities. Prices come from CART_PRODUCTS at checkout, so a price change before checkout is charged at the new price. To keep the price from the time of adding, store it in another attribute such as N003.
  • Keep the rules in the package, not only in the page. The forced ADD request and the withdrawn product were stopped by the package.
  • #SQLERRM_TEXT# as the process error message shows the RAISE_APPLICATION_ERROR text without the ORA- number.
  • Order IDs can skip numbers because identity values are cached. Do not use them as an unbroken sequence.
  • The page has no authorization scheme in this example. In a real shop, add one to the pages and limit who can check out.

Related Guides

Conclusion

A shopping cart in Oracle APEX needs no cart table. An APEX collection keeps the cart for each session, a small package adds, changes and removes members, and the checkout turns the collection into an order and its lines in one call.

Cards with an Add to Cart button post the product to the server, an editable grid on APEX_COLLECTIONS changes quantities, and a validation and the package refuse empty carts, inactive products and wrong quantities. The same pattern works for any list users collect before they save it.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted