Skip to content

Commit 7c8274d

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

File tree

2 files changed

+263
-10
lines changed

2 files changed

+263
-10
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 -> makeBid(bid, bidResponse.getCur(), errors))
205+
.filter(Objects::nonNull)
200206
.toList();
201207
}
202208

203-
private static BidType getBidType(Bid bid) {
209+
private static BidderBid makeBid(Bid bid, String currency, List<BidderError> errors) {
210+
final BidType bidType = getBidType(bid, errors);
211+
return bidType != null ? BidderBid.of(resolvePriceMacros(bid), bidType, currency) : null;
212+
}
213+
214+
private static BidType getBidType(Bid bid, List<BidderError> errors) {
204215
return switch (bid.getMtype()) {
205216
case 1 -> BidType.banner;
206217
case 2 -> BidType.video;
207218
case 4 -> BidType.xNative;
208-
case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype()));
219+
case null, default -> {
220+
errors.add(BidderError.badServerResponse(
221+
"could not define media type for impression: " + bid.getImpid()));
222+
yield null;
223+
}
209224
};
210225
}
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: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
2525
import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk;
2626

27+
import java.math.BigDecimal;
2728
import java.util.Arrays;
2829
import java.util.List;
2930
import java.util.Set;
@@ -33,6 +34,7 @@
3334
import static java.util.function.UnaryOperator.identity;
3435
import static org.assertj.core.api.Assertions.assertThat;
3536
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
37+
import static org.assertj.core.api.Assertions.tuple;
3638
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
3739
import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
3840
import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
@@ -458,7 +460,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
458460
}
459461

460462
@Test
461-
public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
463+
public void makeBidsShouldReturnErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
462464
// given
463465
final BidderCall<BidRequest> httpCall = givenHttpCall(
464466
givenBidResponse(bidBuilder -> bidBuilder.impid("123")));
@@ -469,7 +471,234 @@ public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessi
469471
// then
470472
assertThat(result.getValue()).isEmpty();
471473
assertThat(result.getErrors()).hasSize(1)
472-
.containsOnly(BidderError.badServerResponse("unsupported mtype: null"));
474+
.containsOnly(BidderError.badServerResponse("could not define media type for impression: 123"));
475+
}
476+
477+
@Test
478+
public void makeBidsShouldReturnValidBidsAndErrorsForMixedMediaTypes() throws JsonProcessingException {
479+
// given
480+
final BidderCall<BidRequest> httpCall = givenHttpCall(
481+
mapper.writeValueAsString(BidResponse.builder()
482+
.cur("USD")
483+
.seatbid(singletonList(SeatBid.builder()
484+
.bid(Arrays.asList(
485+
Bid.builder().mtype(1).impid("valid1").build(), // valid banner
486+
Bid.builder().mtype(3).impid("invalid1").build(), // invalid mtype
487+
Bid.builder().mtype(2).impid("valid2").build(), // valid video
488+
Bid.builder().mtype(null).impid("invalid2").build() // null mtype
489+
))
490+
.build()))
491+
.build()));
492+
493+
// when
494+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
495+
496+
// then
497+
assertThat(result.getValue()).hasSize(2)
498+
.extracting(bidderBid -> bidderBid.getBid().getImpid())
499+
.containsExactly("valid1", "valid2");
500+
assertThat(result.getErrors()).hasSize(2)
501+
.containsExactly(
502+
BidderError.badServerResponse("could not define media type for impression: invalid1"),
503+
BidderError.badServerResponse("could not define media type for impression: invalid2"));
504+
}
505+
506+
@Test
507+
public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws JsonProcessingException {
508+
// given
509+
final BidderCall<BidRequest> httpCall = givenHttpCall(
510+
givenBidResponse(bidBuilder -> bidBuilder
511+
.mtype(1)
512+
.impid("123")
513+
.price(BigDecimal.valueOf(1.23))
514+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
515+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
516+
517+
// when
518+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
519+
520+
// then
521+
assertThat(result.getErrors()).isEmpty();
522+
assertThat(result.getValue()).hasSize(1)
523+
.extracting(BidderBid::getBid)
524+
.extracting(Bid::getNurl, Bid::getAdm, Bid::getPrice)
525+
.containsOnly(tuple("http://example.com/nurl?price=1.23", "<div>Price: 1.23</div>", BigDecimal.valueOf(1.23)));
526+
}
527+
528+
@Test
529+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws JsonProcessingException {
530+
// given
531+
final BidderCall<BidRequest> httpCall = givenHttpCall(
532+
givenBidResponse(bidBuilder -> bidBuilder
533+
.mtype(1)
534+
.impid("123")
535+
.price(null)
536+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
537+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
538+
539+
// when
540+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
541+
542+
// then
543+
assertThat(result.getErrors()).isEmpty();
544+
assertThat(result.getValue()).hasSize(1)
545+
.extracting(BidderBid::getBid)
546+
.extracting(Bid::getNurl, Bid::getAdm)
547+
.containsOnly(tuple("http://example.com/nurl?price=0", "<div>Price: 0</div>"));
548+
}
549+
550+
@Test
551+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws JsonProcessingException {
552+
// given
553+
final BidderCall<BidRequest> httpCall = givenHttpCall(
554+
givenBidResponse(bidBuilder -> bidBuilder
555+
.mtype(1)
556+
.impid("123")
557+
.price(BigDecimal.ZERO)
558+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
559+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
560+
561+
// when
562+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
563+
564+
// then
565+
assertThat(result.getErrors()).isEmpty();
566+
assertThat(result.getValue()).hasSize(1)
567+
.extracting(BidderBid::getBid)
568+
.extracting(Bid::getNurl, Bid::getAdm)
569+
.containsOnly(tuple("http://example.com/nurl?price=0", "<div>Price: 0</div>"));
570+
}
571+
572+
@Test
573+
public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro() throws JsonProcessingException {
574+
// given
575+
final BidderCall<BidRequest> httpCall = givenHttpCall(
576+
givenBidResponse(bidBuilder -> bidBuilder
577+
.mtype(1)
578+
.impid("123")
579+
.price(BigDecimal.valueOf(5.67))
580+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
581+
.adm("<div>No macro here</div>")));
582+
583+
// when
584+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
585+
586+
// then
587+
assertThat(result.getErrors()).isEmpty();
588+
assertThat(result.getValue()).hasSize(1)
589+
.extracting(BidderBid::getBid)
590+
.extracting(Bid::getNurl, Bid::getAdm)
591+
.containsOnly(tuple("http://example.com/nurl?price=5.67", "<div>No macro here</div>"));
592+
}
593+
594+
@Test
595+
public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro() throws JsonProcessingException {
596+
// given
597+
final BidderCall<BidRequest> httpCall = givenHttpCall(
598+
givenBidResponse(bidBuilder -> bidBuilder
599+
.mtype(1)
600+
.impid("123")
601+
.price(BigDecimal.valueOf(8.90))
602+
.nurl("http://example.com/nurl")
603+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
604+
605+
// when
606+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
607+
608+
// then
609+
assertThat(result.getErrors()).isEmpty();
610+
assertThat(result.getValue()).hasSize(1)
611+
.extracting(BidderBid::getBid)
612+
.extracting(Bid::getNurl, Bid::getAdm)
613+
.containsOnly(tuple("http://example.com/nurl", "<div>Price: 8.9</div>"));
614+
}
615+
616+
@Test
617+
public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() throws JsonProcessingException {
618+
// given
619+
final BidderCall<BidRequest> httpCall = givenHttpCall(
620+
givenBidResponse(bidBuilder -> bidBuilder
621+
.mtype(1)
622+
.impid("123")
623+
.price(BigDecimal.valueOf(12.34))
624+
.nurl("http://example.com/nurl")
625+
.adm("<div>No macro</div>")));
626+
627+
// when
628+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
629+
630+
// then
631+
assertThat(result.getErrors()).isEmpty();
632+
assertThat(result.getValue()).hasSize(1)
633+
.extracting(BidderBid::getBid)
634+
.extracting(Bid::getNurl, Bid::getAdm)
635+
.containsOnly(tuple("http://example.com/nurl", "<div>No macro</div>"));
636+
}
637+
638+
@Test
639+
public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException {
640+
// given
641+
final BidderCall<BidRequest> httpCall = givenHttpCall(
642+
givenBidResponse(bidBuilder -> bidBuilder
643+
.mtype(1)
644+
.impid("123")
645+
.price(BigDecimal.valueOf(15.00))
646+
.nurl(null)
647+
.adm(null)));
648+
649+
// when
650+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
651+
652+
// then
653+
assertThat(result.getErrors()).isEmpty();
654+
assertThat(result.getValue()).hasSize(1)
655+
.extracting(BidderBid::getBid)
656+
.extracting(Bid::getNurl, Bid::getAdm)
657+
.containsOnly(tuple(null, null));
658+
}
659+
660+
@Test
661+
public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonProcessingException {
662+
// given
663+
final BidderCall<BidRequest> httpCall = givenHttpCall(
664+
givenBidResponse(bidBuilder -> bidBuilder
665+
.mtype(1)
666+
.impid("123")
667+
.price(BigDecimal.valueOf(9.99))
668+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}&backup_price=${AUCTION_PRICE}")
669+
.adm("<div>Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}</div>")));
670+
671+
// when
672+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
673+
674+
// then
675+
assertThat(result.getErrors()).isEmpty();
676+
assertThat(result.getValue()).hasSize(1)
677+
.extracting(BidderBid::getBid)
678+
.extracting(Bid::getNurl, Bid::getAdm)
679+
.containsOnly(tuple("http://example.com/nurl?price=9.99&backup_price=9.99", "<div>Price: 9.99, Fallback: 9.99</div>"));
680+
}
681+
682+
@Test
683+
public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingException {
684+
// given
685+
final BidderCall<BidRequest> httpCall = givenHttpCall(
686+
givenBidResponse(bidBuilder -> bidBuilder
687+
.mtype(1)
688+
.impid("123")
689+
.price(new BigDecimal("123456789.123456789"))
690+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
691+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
692+
693+
// when
694+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
695+
696+
// then
697+
assertThat(result.getErrors()).isEmpty();
698+
assertThat(result.getValue()).hasSize(1)
699+
.extracting(BidderBid::getBid)
700+
.extracting(Bid::getNurl, Bid::getAdm)
701+
.containsOnly(tuple("http://example.com/nurl?price=123456789.123456789", "<div>Price: 123456789.123456789</div>"));
473702
}
474703

475704
private String givenBidResponse(UnaryOperator<Bid.BidBuilder> bidCustomizer) throws JsonProcessingException {
@@ -508,5 +737,4 @@ private static ObjectNode impExt(String publisherId) {
508737
private static ObjectNode impExt(String publisherId, String supplySourceId) {
509738
return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId, supplySourceId)));
510739
}
511-
512740
}

0 commit comments

Comments
 (0)