Node.js Go Python Rust Java RubyCopy const BASE = ' https://sandbox-api.gozem.co/courier ' ;
const token = process . env . GOZEM_ACCESS_TOKEN ;
async function call ( method , path , body ) {
const res = await fetch ( BASE + path , {
method ,
headers : {
Authorization : ` Bearer ${ token } ` ,
... ( body ? { ' Content-Type ' : ' application/json ' } : {} ) ,
} ,
body : body ? JSON . stringify ( body ) : undefined ,
} );
const payload = await res . json ();
if ( ! res . ok ) throw new Error ( ` ${ res . status } ${ payload . code } : ${ payload . message } ` );
return payload . data ;
}
/** One sandbox trip, one dropoff. Enough to drive every transition. */
const bookTrip = () =>
call ( ' POST ' , ' /v1/trips ' , {
title : ' Simulation test ' ,
vehicle : ' motorcycle ' ,
pickup : {
label : ' Raku Raku Express HQ - Zone Aéroport ' ,
lat : 6.166245 ,
lon : 1.247951 ,
contact_name : ' Raku Raku Express ' ,
contact_phone : ' +22822200011 ' ,
},
dropoffs : [
{
label : ' Pharmacie Tokoin Forever ' ,
lat : 6.172834 ,
lon : 1.231456 ,
contact_name : ' Kossi Mensah ' ,
contact_phone : ' +22893112233 ' ,
},
] ,
} );
/** Returns the trip as it stands after the action, not an acknowledgement. */
const advance = ( tripId , action , extra = {} ) =>
call ( ' POST ' , ` /v1/trips/ ${ tripId } /simulation/state ` , { action , ... extra } );
const trip = await bookTrip ();
// Each of these fires the webhook a real trip would. Check your handler between them.
// `assign` picks an available sandbox driver for you; the trip comes back with it.
const assigned = await advance ( trip . guid , ' assign ' ); // courier.trip.assigned
console . log ( assigned . driver . name , assigned . driver . vehicle_plate );
await advance ( trip . guid , ' arrive-pickup ' ); // courier.trip.at_pickup
await advance ( trip . guid , ' start ' ); // courier.trip.started
// One pair per dropoff, before complete. They fire even on a single-stop trip, so
// handling them now means multi-stop needs no new code later.
await advance ( trip . guid , ' stop-start ' ); // courier.trip.stop.started
await advance ( trip . guid , ' stop-complete ' ); // courier.trip.stop.completed
const done = await advance ( trip . guid , ' complete ' ); // courier.trip.completed
// If this says completed and your own records do not, the gap is in your handler.
console . log ( done . status , done . completed_at );
// The read endpoint is there when you want the trip outside a simulation call.
const fetched = await call ( ' GET ' , ` /v1/trips/ ${ trip . guid } ` );
console . log ( fetched . status );
// cancel and expire are terminal, so each needs its own trip.
export async function exerciseFailures () {
const canceled = await bookTrip ();
await advance ( canceled . guid , ' assign ' );
await advance ( canceled . guid , ' cancel ' , { reason: ' Recipient unreachable ' });
const expired = await bookTrip ();
await advance ( expired . guid , ' expire ' );
} package main
import (
" bytes "
" encoding/json "
" fmt "
" net/http "
" os "
)
const base = " https://sandbox-api.gozem.co/courier "
var token = os . Getenv ( " GOZEM_ACCESS_TOKEN " )
type envelope[ T any] struct {
Data T ` json:"data" `
Code string ` json:"code" `
Message string ` json:"message" `
}
func call [ T any]( method , path string , body any) (T, error ) {
var out envelope[T]
var payload [] byte
if body != nil {
payload , _ = json . Marshal ( body )
}
req , err := http . NewRequest ( method , base + path , bytes . NewReader ( payload ))
if err != nil {
return out . Data , err
}
req . Header . Set ( " Authorization " , " Bearer " + token )
if body != nil {
req . Header . Set ( " Content-Type " , " application/json " )
}
res , err := http . DefaultClient . Do ( req )
if err != nil {
return out . Data , err
}
defer res . Body . Close ()
if err := json . NewDecoder ( res . Body ). Decode ( & out ); err != nil {
return out . Data , err
}
if res . StatusCode >= 400 {
return out . Data , fmt . Errorf ( " %d %s : %s " , res . StatusCode , out . Code , out . Message )
}
return out . Data , nil
}
type Driver struct {
Name string ` json:"name" `
VehiclePlate string ` json:"vehicle_plate" `
}
type Trip struct {
GUID string ` json:"guid" `
Status string ` json:"status" `
CompletedAt string ` json:"completed_at" `
Driver Driver ` json:"driver" `
}
// One sandbox trip, one dropoff. Enough to drive every transition.
func bookTrip () (Trip, error ) {
return call [Trip]( " POST " , " /v1/trips " , map [ string ]any{
" title " : " Simulation test " ,
" vehicle " : " motorcycle " ,
" pickup " : map [ string ]any{
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 " ,
},
" dropoffs " : [] map [ string ]any{{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 " ,
}},
})
}
// Returns the trip as it stands after the action, not an acknowledgement.
func advance ( tripID , action string , extra map [ string ]any) (Trip, error ) {
body := map [ string ]any{ " action " : action }
for k , v := range extra {
body [ k ] = v
}
return call [Trip]( " POST " , " /v1/trips/ " + tripID + " /simulation/state " , body )
}
func run () error {
trip , err := bookTrip ()
if err != nil {
return err
}
// Each of these fires the webhook a real trip would. Check your handler between
// them. `assign` picks an available sandbox driver for you; the trip comes back
// with it. The stop pair goes once per dropoff, before complete, and fires even on
// a single-stop trip, so handling it now means multi-stop needs no new code later.
assigned , err := advance ( trip . GUID , " assign " , nil )
if err != nil {
return err
}
fmt . Println ( assigned . Driver . Name , assigned . Driver . VehiclePlate )
for _ , action := range [] string {
" arrive-pickup " , // courier.trip.at_pickup
" start " , // courier.trip.started
" stop-start " , // courier.trip.stop.started
" stop-complete " , // courier.trip.stop.completed
} {
if _ , err := advance ( trip . GUID , action , nil ); err != nil {
return err
}
}
done , err := advance ( trip . GUID , " complete " , nil ) // courier.trip.completed
if err != nil {
return err
}
// If this says completed and your own records do not, the gap is in your handler.
fmt . Println ( done . Status , done . CompletedAt )
// The read endpoint is there when you want the trip outside a simulation call.
fetched , err := call [Trip]( " GET " , " /v1/trips/ " + trip . GUID , nil )
if err != nil {
return err
}
fmt . Println ( fetched . Status )
return nil
}
// cancel and expire are terminal, so each needs its own trip.
func exerciseFailures () error {
canceled , err := bookTrip ()
if err != nil {
return err
}
if _ , err := advance ( canceled . GUID , " assign " , nil ); err != nil {
return err
}
if _ , err := advance ( canceled . GUID , " cancel " , map [ string ]any{
" reason " : " Recipient unreachable " ,
}); err != nil {
return err
}
expired , err := bookTrip ()
if err != nil {
return err
}
_ , err = advance ( expired . GUID , " expire " , nil )
return err
}
import os
import requests
BASE = " https://sandbox-api.gozem.co/courier "
token = os.environ[ " GOZEM_ACCESS_TOKEN " ]
def call ( method , path , body= None ) :
res = requests. request (
method ,
BASE + path ,
headers = { " Authorization " : f "Bearer {token} " } ,
json = body ,
timeout = 30 ,
)
payload = res. json ()
if not res.ok:
raise RuntimeError ( f " {res.status_code} {payload [ ' code ' ] } : {payload [ ' message ' ] } " )
return payload[ " data " ]
def book_trip () :
""" One sandbox trip, one dropoff. Enough to drive every transition. """
return call (
" POST " ,
" /v1/trips " ,
{
" title " : " Simulation test " ,
" vehicle " : " motorcycle " ,
" pickup " : {
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 " ,
},
" dropoffs " : [
{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 " ,
}
] ,
} ,
)
def advance ( trip_id , action , **extra ) :
""" Returns the trip as it stands after the action, not an acknowledgement. """
return call (
" POST " , f "/v1/trips/ {trip_id} /simulation/state" , { " action " : action, ** extra}
)
trip = book_trip ()
# Each of these fires the webhook a real trip would. Check your handler between them.
# `assign` picks an available sandbox driver for you; the trip comes back with it.
assigned = advance ( trip [ " guid " ] , " assign " ) # courier.trip.assigned
print ( assigned [ " driver " ] [ " name " ], assigned [ " driver " ] [ " vehicle_plate " ] )
# One stop pair per dropoff, before complete. They fire even on a single-stop trip, so
# handling them now means multi-stop needs no new code later.
for action in [ " arrive-pickup " , " start " , " stop-start " , " stop-complete " ] :
advance ( trip [ " guid " ] , action )
done = advance ( trip [ " guid " ] , " complete " ) # courier.trip.completed
# If this says completed and your own records do not, the gap is in your handler.
print ( done [ " status " ] , done [ " completed_at " ])
# The read endpoint is there when you want the trip outside a simulation call.
fetched = call ( " GET " , f "/v1/trips/ {trip [ ' guid ' ] } " )
print ( fetched [ " status " ])
def exercise_failures () :
""" cancel and expire are terminal, so each needs its own trip. """
canceled = book_trip ()
advance ( canceled [ " guid " ] , " assign " )
advance ( canceled [ " guid " ] , " cancel " , reason = " Recipient unreachable " )
expired = book_trip ()
advance ( expired [ " guid " ] , " expire " ) use serde :: Deserialize;
use serde_json :: {json, Map, Value};
const BASE : & str = " https://sandbox-api.gozem.co/courier " ;
#[derive(Deserialize)]
struct Envelope<T> {
data : Option<T>,
code : Option<String>,
message : Option<String>,
}
#[derive(Deserialize)]
struct Driver {
name : String,
vehicle_plate : String,
}
#[derive(Deserialize)]
struct Trip {
guid : String,
status : String,
completed_at : Option<String>,
driver : Option<Driver>,
}
async fn call <T : for <'de> Deserialize<'de>>(
client : & reqwest :: Client,
method : reqwest :: Method,
path : & str,
body : Option<Value>,
) -> anyhow :: Result<T> {
let token = std :: env :: var ( " GOZEM_ACCESS_TOKEN " ) ? ;
let mut request = client . request ( method , format! ( " {BASE}{path} " )) . bearer_auth ( token );
if let Some( body ) = body {
request = request . json ( & body );
}
let response = request . send () . await ? ;
let status = response . status ();
let envelope : Envelope<T> = response . json () . await ? ;
if ! status . is_success () {
anyhow :: bail! (
" {} {}: {} " ,
status . as_u16 (),
envelope . code . unwrap_or_default (),
envelope . message . unwrap_or_default ()
);
}
envelope . data . ok_or_else ( || anyhow :: anyhow! ( " no data in response " ))
}
/// One sandbox trip, one dropoff. Enough to drive every transition.
async fn book_trip ( client : & reqwest :: Client) -> anyhow :: Result<Trip> {
call (
client ,
reqwest :: Method :: POST ,
" /v1/trips " ,
Some( json! ({
" title " : " Simulation test " ,
" vehicle " : " motorcycle " ,
" pickup " : {
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 "
},
" dropoffs " : [{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 "
}]
})),
)
. await
}
/// Returns the trip as it stands after the action, not an acknowledgement.
async fn advance (
client : & reqwest :: Client,
trip_id : & str,
action : & str,
extra : Map<String, Value>,
) -> anyhow :: Result<Trip> {
let mut body = extra ;
body . insert ( " action " . into (), json! ( action ));
call (
client ,
reqwest :: Method :: POST ,
& format! ( " /v1/trips/{trip_id}/simulation/state " ),
Some(Value :: Object ( body )),
)
. await
}
async fn run ( client : & reqwest :: Client) -> anyhow :: Result<()> {
let trip = book_trip ( client ) . await ? ;
// Each of these fires the webhook a real trip would. Check your handler between
// them. `assign` picks an available sandbox driver for you; the trip comes back
// with it. The stop pair goes once per dropoff, before complete, and fires even on
// a single-stop trip, so handling it now means multi-stop needs no new code later.
let assigned = advance ( client , & trip . guid, " assign " , Map :: new ()) . await ? ;
if let Some( driver ) = assigned . driver {
println! ( " {} {} " , driver . name, driver . vehicle_plate);
}
for action in [ " arrive-pickup " , " start " , " stop-start " , " stop-complete " ] {
advance ( client , & trip . guid, action , Map :: new ()) . await ? ;
}
let done = advance ( client , & trip . guid, " complete " , Map :: new ()) . await ? ;
// If this says completed and your own records do not, the gap is in your handler.
println! ( " {} {:?} " , done . status, done . completed_at);
// The read endpoint is there when you want the trip outside a simulation call.
let fetched : Trip = call (
client ,
reqwest :: Method :: GET ,
& format! ( " /v1/trips/{} " , trip . guid),
None,
)
. await ? ;
println! ( " {} " , fetched . status);
Ok(())
}
/// cancel and expire are terminal, so each needs its own trip.
async fn exercise_failures ( client : & reqwest :: Client) -> anyhow :: Result<()> {
let canceled = book_trip ( client ) . await ? ;
advance ( client , & canceled . guid, " assign " , Map :: new ()) . await ? ;
let mut reason = Map :: new ();
reason . insert ( " reason " . into (), json! ( " Recipient unreachable " ));
advance ( client , & canceled . guid, " cancel " , reason ) . await ? ;
let expired = book_trip ( client ) . await ? ;
advance ( client , & expired . guid, " expire " , Map :: new ()) . await ? ;
Ok(())
} import com . fasterxml . jackson . databind . JsonNode ;
import com . fasterxml . jackson . databind . ObjectMapper ;
import java . net . URI ;
import java . net . http . HttpClient ;
import java . net . http . HttpRequest ;
import java . net . http . HttpResponse ;
import java . time . Duration ;
import java . util . HashMap ;
import java . util . List ;
import java . util . Map ;
public class SimulateATrip {
private static final String BASE = " https://sandbox-api.gozem.co/courier " ;
private static final String TOKEN = System . getenv ( " GOZEM_ACCESS_TOKEN " ) ;
private static final ObjectMapper MAPPER = new ObjectMapper () ;
private static final HttpClient CLIENT = HttpClient . newHttpClient () ;
static JsonNode call ( String method , String path , Object body ) throws Exception {
HttpRequest . BodyPublisher payload =
body == null
? HttpRequest . BodyPublishers . noBody ()
: HttpRequest . BodyPublishers . ofString ( MAPPER . writeValueAsString ( body )) ;
HttpRequest . Builder request =
HttpRequest . newBuilder ()
. uri ( URI . create ( BASE + path ))
. header ( " Authorization " , " Bearer " + TOKEN )
. timeout ( Duration . ofSeconds ( 30 ))
. method ( method, payload ) ;
if (body != null ) request . header ( " Content-Type " , " application/json " ) ;
HttpResponse < String > response =
CLIENT . send ( request . build () , HttpResponse . BodyHandlers . ofString ()) ;
JsonNode envelope = MAPPER . readTree ( response . body ()) ;
if ( response . statusCode () >= 400 ) {
throw new RuntimeException (
response . statusCode () + " " + envelope . path ( " code " ) . asText ()
+ " : " + envelope . path ( " message " ) . asText ()) ;
}
return envelope . get ( " data " ) ;
}
/** One sandbox trip, one dropoff. Enough to drive every transition. */
static JsonNode bookTrip () throws Exception {
return call (
" POST " ,
" /v1/trips " ,
Map . of (
" title " , " Simulation test " ,
" vehicle " , " motorcycle " ,
" pickup " ,
Map . of (
" label " , " Raku Raku Express HQ - Zone Aéroport " ,
" lat " , 6.166245 ,
" lon " , 1.247951 ,
" contact_name " , " Raku Raku Express " ,
" contact_phone " , " +22822200011 " ) ,
" dropoffs " ,
List . of (
Map . of (
" label " , " Pharmacie Tokoin Forever " ,
" lat " , 6.172834 ,
" lon " , 1.231456 ,
" contact_name " , " Kossi Mensah " ,
" contact_phone " , " +22893112233 " )))) ;
}
/** Returns the trip as it stands after the action, not an acknowledgement. */
static JsonNode advance ( String tripId , String action , Map < String , Object > extra )
throws Exception {
Map < String , Object > body = new HashMap <>(extra);
body . put ( " action " , action ) ;
return call ( " POST " , " /v1/trips/ " + tripId + " /simulation/state " , body ) ;
}
static void run () throws Exception {
JsonNode trip = bookTrip () ;
String tripId = trip . get ( " guid " ) . asText () ;
// Each of these fires the webhook a real trip would. Check your handler between
// them. `assign` picks an available sandbox driver for you; the trip comes back
// with it. The stop pair goes once per dropoff, before complete, and fires even on
// a single-stop trip, so handling it now means multi-stop needs no new code later.
JsonNode assigned = advance ( tripId, " assign " , Map . of ()) ; // courier.trip.assigned
JsonNode driver = assigned . get ( " driver " ) ;
System . out . println ( driver . get ( " name " ) . asText () + " " + driver . get ( " vehicle_plate " ) . asText ()) ;
for ( String action : List . of ( " arrive-pickup " , " start " , " stop-start " , " stop-complete " ) ) {
advance ( tripId, action, Map . of ()) ;
}
JsonNode done = advance ( tripId, " complete " , Map . of ()) ; // courier.trip.completed
// If this says completed and your own records do not, the gap is in your handler.
System . out . println ( done . get ( " status " ) . asText () + " " + done . path ( " completed_at " ) . asText ()) ;
// The read endpoint is there when you want the trip outside a simulation call.
JsonNode fetched = call ( " GET " , " /v1/trips/ " + tripId, null ) ;
System . out . println ( fetched . get ( " status " ) . asText ()) ;
}
/** cancel and expire are terminal, so each needs its own trip. */
static void exerciseFailures () throws Exception {
JsonNode canceled = bookTrip () ;
advance ( canceled . get ( " guid " ) . asText () , " assign " , Map . of ()) ;
advance (
canceled . get ( " guid " ) . asText () , " cancel " , Map . of ( " reason " , " Recipient unreachable " )) ;
JsonNode expired = bookTrip () ;
advance ( expired . get ( " guid " ) . asText () , " expire " , Map . of ()) ;
}
}