Skip to content

Commit e8cbef1

Browse files
arg-resolve-auction-price-macro
1 parent ea32800 commit e8cbef1

File tree

2 files changed

+264
-9
lines changed

2 files changed

+264
-9
lines changed

src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.prebid.server.util.BidderUtil;
3030
import org.prebid.server.util.HttpUtil;
3131

32+
import java.math.BigDecimal;
3233
import java.util.ArrayList;
3334
import java.util.Collection;
3435
import java.util.Collections;
@@ -44,6 +45,7 @@ public class TheTradeDeskBidder implements Bidder<BidRequest> {
4445

4546
private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
4647
private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$");
48+
private static final String PRICE_MACRO = "${AUCTION_PRICE}";
4749

4850
private final String endpointUrl;
4951
private final String supplyId;
@@ -180,32 +182,55 @@ private String resolveEndpoint(String sourceSupplyId) {
180182
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
181183
try {
182184
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
183-
return Result.withValues(extractBids(bidResponse));
184-
} catch (DecodeException | PreBidException e) {
185+
final List<BidderError> errors = new ArrayList<>();
186+
final List<BidderBid> bids = extractBids(bidResponse, errors);
187+
return Result.of(bids, errors);
188+
} catch (DecodeException e) {
185189
return Result.withError(BidderError.badServerResponse(e.getMessage()));
186190
}
187191
}
188192

189-
private static List<BidderBid> extractBids(BidResponse bidResponse) {
193+
private static List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
190194
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
191195
return Collections.emptyList();
192196
}
193197

194198
return bidResponse.getSeatbid().stream()
195199
.filter(Objects::nonNull)
196-
.map(SeatBid::getBid).filter(Objects::nonNull)
200+
.map(SeatBid::getBid)
201+
.filter(Objects::nonNull)
197202
.flatMap(Collection::stream)
198203
.filter(Objects::nonNull)
199-
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
204+
.map(bid -> {
205+
final BidType bidType = getBidType(bid, errors);
206+
return bidType != null ? BidderBid.of(resolvePriceMacros(bid), bidType, bidResponse.getCur()) : null;
207+
})
208+
.filter(Objects::nonNull)
200209
.toList();
201210
}
202211

203-
private static BidType getBidType(Bid bid) {
212+
private static BidType getBidType(Bid bid, List<BidderError> errors) {
204213
return switch (bid.getMtype()) {
205214
case 1 -> BidType.banner;
206215
case 2 -> BidType.video;
207216
case 4 -> BidType.xNative;
208-
case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype()));
217+
case null, default -> {
218+
errors.add(BidderError.badServerResponse(
219+
"could not define media type for impression: " + bid.getImpid()));
220+
yield null;
221+
}
209222
};
210223
}
224+
225+
226+
227+
private static Bid resolvePriceMacros(Bid bid) {
228+
final BigDecimal price = bid.getPrice();
229+
final String priceAsString = price != null ? price.toPlainString() : "0";
230+
231+
return bid.toBuilder()
232+
.nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString))
233+
.adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString))
234+
.build();
235+
}
211236
}

src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.node.ObjectNode;
5+
import java.math.BigDecimal;
56
import com.iab.openrtb.request.App;
67
import com.iab.openrtb.request.Banner;
78
import com.iab.openrtb.request.BidRequest;
@@ -458,7 +459,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
458459
}
459460

460461
@Test
461-
public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
462+
public void makeBidsShouldReturnErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
462463
// given
463464
final BidderCall<BidRequest> httpCall = givenHttpCall(
464465
givenBidResponse(bidBuilder -> bidBuilder.impid("123")));
@@ -469,7 +470,36 @@ public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessi
469470
// then
470471
assertThat(result.getValue()).isEmpty();
471472
assertThat(result.getErrors()).hasSize(1)
472-
.containsOnly(BidderError.badServerResponse("unsupported mtype: null"));
473+
.containsOnly(BidderError.badServerResponse("could not define media type for impression: 123"));
474+
}
475+
476+
@Test
477+
public void makeBidsShouldReturnValidBidsAndErrorsForMixedMediaTypes() throws JsonProcessingException {
478+
// given
479+
final BidderCall<BidRequest> httpCall = givenHttpCall(
480+
mapper.writeValueAsString(BidResponse.builder()
481+
.cur("USD")
482+
.seatbid(singletonList(SeatBid.builder()
483+
.bid(Arrays.asList(
484+
Bid.builder().mtype(1).impid("valid1").build(), // valid banner
485+
Bid.builder().mtype(3).impid("invalid1").build(), // invalid mtype
486+
Bid.builder().mtype(2).impid("valid2").build(), // valid video
487+
Bid.builder().mtype(null).impid("invalid2").build() // null mtype
488+
))
489+
.build()))
490+
.build()));
491+
492+
// when
493+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
494+
495+
// then
496+
assertThat(result.getValue()).hasSize(2)
497+
.extracting(bidderBid -> bidderBid.getBid().getImpid())
498+
.containsExactly("valid1", "valid2");
499+
assertThat(result.getErrors()).hasSize(2)
500+
.containsExactly(
501+
BidderError.badServerResponse("could not define media type for impression: invalid1"),
502+
BidderError.badServerResponse("could not define media type for impression: invalid2"));
473503
}
474504

475505
private String givenBidResponse(UnaryOperator<Bid.BidBuilder> bidCustomizer) throws JsonProcessingException {
@@ -509,4 +539,204 @@ private static ObjectNode impExt(String publisherId, String supplySourceId) {
509539
return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId, supplySourceId)));
510540
}
511541

542+
543+
@Test
544+
public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws JsonProcessingException {
545+
// given
546+
final BidderCall<BidRequest> httpCall = givenHttpCall(
547+
givenBidResponse(bidBuilder -> bidBuilder
548+
.mtype(1)
549+
.impid("123")
550+
.price(BigDecimal.valueOf(1.23))
551+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
552+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
553+
554+
// when
555+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
556+
557+
// then
558+
assertThat(result.getErrors()).isEmpty();
559+
assertThat(result.getValue()).hasSize(1);
560+
final BidderBid bidderBid = result.getValue().getFirst();
561+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=1.23");
562+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 1.23</div>");
563+
assertThat(bidderBid.getBid().getPrice()).isEqualTo(BigDecimal.valueOf(1.23));
564+
}
565+
566+
@Test
567+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws JsonProcessingException {
568+
// given
569+
final BidderCall<BidRequest> httpCall = givenHttpCall(
570+
givenBidResponse(bidBuilder -> bidBuilder
571+
.mtype(1)
572+
.impid("123")
573+
.price(null)
574+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
575+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
576+
577+
// when
578+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
579+
580+
// then
581+
assertThat(result.getErrors()).isEmpty();
582+
assertThat(result.getValue()).hasSize(1);
583+
final BidderBid bidderBid = result.getValue().getFirst();
584+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=0");
585+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 0</div>");
586+
}
587+
588+
@Test
589+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws JsonProcessingException {
590+
// given
591+
final BidderCall<BidRequest> httpCall = givenHttpCall(
592+
givenBidResponse(bidBuilder -> bidBuilder
593+
.mtype(1)
594+
.impid("123")
595+
.price(BigDecimal.ZERO)
596+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
597+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
598+
599+
// when
600+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
601+
602+
// then
603+
assertThat(result.getErrors()).isEmpty();
604+
assertThat(result.getValue()).hasSize(1);
605+
final BidderBid bidderBid = result.getValue().getFirst();
606+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=0");
607+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 0</div>");
608+
}
609+
610+
@Test
611+
public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro() throws JsonProcessingException {
612+
// given
613+
final BidderCall<BidRequest> httpCall = givenHttpCall(
614+
givenBidResponse(bidBuilder -> bidBuilder
615+
.mtype(1)
616+
.impid("123")
617+
.price(BigDecimal.valueOf(5.67))
618+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
619+
.adm("<div>No macro here</div>")));
620+
621+
// when
622+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
623+
624+
// then
625+
assertThat(result.getErrors()).isEmpty();
626+
assertThat(result.getValue()).hasSize(1);
627+
final BidderBid bidderBid = result.getValue().getFirst();
628+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=5.67");
629+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>No macro here</div>");
630+
}
631+
632+
@Test
633+
public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro() throws JsonProcessingException {
634+
// given
635+
final BidderCall<BidRequest> httpCall = givenHttpCall(
636+
givenBidResponse(bidBuilder -> bidBuilder
637+
.mtype(1)
638+
.impid("123")
639+
.price(BigDecimal.valueOf(8.90))
640+
.nurl("http://example.com/nurl")
641+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
642+
643+
// when
644+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
645+
646+
// then
647+
assertThat(result.getErrors()).isEmpty();
648+
assertThat(result.getValue()).hasSize(1);
649+
final BidderBid bidderBid = result.getValue().getFirst();
650+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl");
651+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 8.90</div>");
652+
}
653+
654+
@Test
655+
public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() throws JsonProcessingException {
656+
// given
657+
final BidderCall<BidRequest> httpCall = givenHttpCall(
658+
givenBidResponse(bidBuilder -> bidBuilder
659+
.mtype(1)
660+
.impid("123")
661+
.price(BigDecimal.valueOf(12.34))
662+
.nurl("http://example.com/nurl")
663+
.adm("<div>No macro</div>")));
664+
665+
// when
666+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
667+
668+
// then
669+
assertThat(result.getErrors()).isEmpty();
670+
assertThat(result.getValue()).hasSize(1);
671+
final BidderBid bidderBid = result.getValue().getFirst();
672+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl");
673+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>No macro</div>");
674+
}
675+
676+
@Test
677+
public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException {
678+
// given
679+
final BidderCall<BidRequest> httpCall = givenHttpCall(
680+
givenBidResponse(bidBuilder -> bidBuilder
681+
.mtype(1)
682+
.impid("123")
683+
.price(BigDecimal.valueOf(15.00))
684+
.nurl(null)
685+
.adm(null)));
686+
687+
// when
688+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
689+
690+
// then
691+
assertThat(result.getErrors()).isEmpty();
692+
assertThat(result.getValue()).hasSize(1);
693+
final BidderBid bidderBid = result.getValue().getFirst();
694+
assertThat(bidderBid.getBid().getNurl()).isNull();
695+
assertThat(bidderBid.getBid().getAdm()).isNull();
696+
}
697+
698+
@Test
699+
public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonProcessingException {
700+
// given
701+
final BidderCall<BidRequest> httpCall = givenHttpCall(
702+
givenBidResponse(bidBuilder -> bidBuilder
703+
.mtype(1)
704+
.impid("123")
705+
.price(BigDecimal.valueOf(9.99))
706+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}&backup_price=${AUCTION_PRICE}")
707+
.adm("<div>Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}</div>")));
708+
709+
// when
710+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
711+
712+
// then
713+
assertThat(result.getErrors()).isEmpty();
714+
assertThat(result.getValue()).hasSize(1);
715+
final BidderBid bidderBid = result.getValue().getFirst();
716+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=9.99&backup_price=9.99");
717+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 9.99, Fallback: 9.99</div>");
718+
}
719+
720+
@Test
721+
public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingException {
722+
// given
723+
final BidderCall<BidRequest> httpCall = givenHttpCall(
724+
givenBidResponse(bidBuilder -> bidBuilder
725+
.mtype(1)
726+
.impid("123")
727+
.price(new BigDecimal("123456789.123456789"))
728+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
729+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
730+
731+
// when
732+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
733+
734+
// then
735+
assertThat(result.getErrors()).isEmpty();
736+
assertThat(result.getValue()).hasSize(1);
737+
final BidderBid bidderBid = result.getValue().getFirst();
738+
assertThat(bidderBid.getBid().getNurl()).isEqualTo("http://example.com/nurl?price=123456789.123456789");
739+
assertThat(bidderBid.getBid().getAdm()).isEqualTo("<div>Price: 123456789.123456789</div>");
740+
}
741+
512742
}

0 commit comments

Comments
 (0)