Consignors should see their unit status, inquiry count, and price — but not lead contact information. Designing the data exposure intentionally.
Consignors are a distinct customer type from buyers. A buyer completes a transaction and is done. A consignor has an ongoing relationship with the business — their unit is listed for sale, and they have a legitimate interest in knowing: is it active, what’s the current price, has anyone inquired? The seller portal gives them that visibility without requiring staff involvement on every check-in.
The Anchor build had 30–40 active consignments at any given time. Consignors called and emailed regularly to check on their listing: “Is it still showing? Has anyone looked at it? Can we drop the price?” These questions were answerable from the inventory system, but answering them required a staff member to stop what they were doing, look it up, and reply.
At 30 consignors, even two check-in calls per week per consignor is 60 staff-minutes per week on interruptions. The portal answer: let consignors see it themselves.
The seller portal shows each consignor their unit(s) from the [client]_owned_units table, filtered by relationship = 'consignor'. Each unit card shows live data from the inventory CPT:
function [client]_get_consignor_units( int $user_id ): array {
$lead_id = get_user_meta( $user_id, '[client]_portal_lead_id', true );
if ( ! $lead_id ) return [];
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT ou.*, p.post_status
FROM [client]_owned_units ou
JOIN {$wpdb->posts} p ON p.ID = ou.unit_id
WHERE ou.lead_id = %d
AND ou.relationship = 'consignor'
AND ou.status = 'active'
ORDER BY ou.created_at DESC",
$lead_id
) );
$units = [];
foreach ( $rows as $row ) {
$unit_id = (int) $row->unit_id;
$units[] = [
'owned_unit_id' => $row->id,
'unit_id' => $unit_id,
'title' => get_the_title( $unit_id ),
'status' => $row->post_status === 'publish' ? 'Active' : 'Off Market',
'price' => get_post_meta( $unit_id, '[client]_price', true ),
'days_listed' => (int) ( ( time() - strtotime( $row->created_at ) ) / DAY_IN_SECONDS ),
'inquiry_count' => [client]_get_inquiry_count_for_unit( $unit_id ),
];
}
return $units;
}
Consignors should know how many inquiries their unit has received, but they should not see the contact information or messages of those leads. Only the count is exposed — not the underlying records:
function [client]_get_inquiry_count_for_unit( int $unit_id ): int {
global $wpdb;
return (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM [client]_leads
WHERE source_unit_id = %d
AND status NOT IN ('closed_lost')",
$unit_id
) );
}
Consignors can’t directly change the listing price — that could bypass consignment agreement terms or cause mistakes. Instead they submit a price change request, which creates a CRM note for the sales manager to review:
function [client]_handle_price_change_request( int $user_id, int $unit_id, float $requested_price ): bool {
$lead_id = get_user_meta( $user_id, '[client]_portal_lead_id', true );
if ( ! $lead_id ) return false;
// Verify the unit belongs to this consignor
global $wpdb;
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT id FROM [client]_owned_units
WHERE lead_id = %d AND unit_id = %d AND relationship = 'consignor'",
$lead_id, $unit_id
) );
if ( ! $row ) return false;
$current_price = (float) get_post_meta( $unit_id, '[client]_price', true );
$note = sprintf(
'Consignor requested price change: $%s → $%s (via portal)',
number_format( $current_price, 0 ),
number_format( $requested_price, 0 )
);
[client]_add_conversation( $lead_id, 'note', $note, 'Price change request', 'portal' );
// Notify sales manager
wp_mail(
SALES_MANAGER_EMAIL,
'[Portal] Price change request: ' . get_the_title( $unit_id ),
$note,
[ 'Content-Type: text/html; charset=UTF-8' ]
);
return true;
}
Every portal page uses a gating function that verifies the logged-in user is a portal customer and that the resource they’re requesting belongs to them:
function [client]_verify_portal_access( int $unit_id = 0 ): bool {
if ( ! is_user_logged_in() ) {
wp_redirect( [client]_portal_login_url() );
exit;
}
$user = wp_get_current_user();
if ( ! in_array( '[client]_portal_customer', $user->roles, true ) ) {
wp_redirect( home_url() );
exit;
}
// If a specific unit was requested, verify ownership
if ( $unit_id ) {
$lead_id = get_user_meta( $user->ID, '[client]_portal_lead_id', true );
global $wpdb;
$owns = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM [client]_owned_units WHERE lead_id = %d AND unit_id = %d",
$lead_id, $unit_id
) );
if ( ! $owns ) {
wp_redirect( [client]_portal_home_url() );
exit;
}
}
return true;
}
The seller portal converts a support burden into a self-service feature. Staff time previously spent on status-check calls is redirected to actual sales activities. The consignor gets real-time information without having to catch someone on the phone during business hours.
The inquiry count display — showing how many people have asked about their unit — reduces the emotional frustration of “is anyone even looking?” without giving the consignor access to lead contact information. It’s the right level of transparency: more than they had before, less than could cause problems.
The seller portal reduced consignor “checking in” calls by approximately 70% in the first month. The most-used feature was the listing status display followed by the inquiry count. The price change request form was used 8 times in the first three months — all legitimate, all reviewed and processed within 24 hours by the sales manager.
No consignor attempted to access another consignor’s data in the first six months. The access control layer was verified but never exercised against a real attack. It exists because it should, not because anyone expected malicious use — but portal authorization bugs surface unexpectedly.
update_post_meta(). Business decisions should stay in human hands.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →